- Beibehaltung des auf Go
html/template basierenden serverseitigen Renderings, Ergänzung dynamischer Interaktionen mit HTMX und selektive Rückgabe ganzer Seiten oder HTML-Fragmente über einen einzigen Renderer
- Aufteilung des Markups in
base.tmpl, Seiten-Templates und wiederverwendbare Fragmente; Nutzung von Go 1.16 embed.FS und einer gemeinsamen Template-Menge, um Deployment- und Rendering-Struktur zu vereinfachen
- Rückgabe von Suchergebnissen je nach
HX-Request als ganze Seite oder Tabellenzeilen; Vermeidung von Cache- und Zurück-Navigation-Wiederherstellungsfehlern mit Vary: HX-Request und historyRestoreAsHxRequest: false
- Beim Weiterleiten von HTMX-Requests auf eine andere Seite statt regulärer 3xx-Antworten
HX-Redirect mit einer 2xx-Antwort verwenden; Steuerung von Austauschbereich und Fehleranzeige über responseHandling für 204, 422, 4xx und 5xx
- Eine Konfiguration, die Local-Storage-Cache und Attributvererbung deaktiviert sowie Request-Timeouts setzt, als Ausgangspunkt verwenden; bei wachsendem Umfang können Zwischen-Layout-Templates ergänzt werden, um auch separate Bildschirmstrukturen wie Admin-Bereiche zu unterstützen
Warum Go-Server-Rendering mit HTMX kombinieren?
- HTMX ermöglicht dynamische Interaktionen mit wenig JavaScript und bewahrt zugleich die Konsistenz und Sicherheit des serverseitigen HTML-Renderings mit Go
html/template
- Die Implementierung behandelt drei Achsen
- Eine Template-Struktur, die sowohl ganze Seiten als auch partielle HTML-Antworten unterstützt
- Redirects und Fehlerbehandlung in einer HTMX-Umgebung
- Konfiguration, die HTMX-Defaults ändert, und die Gründe dafür
- Ausgehend von der Grundfunktion, einen Button durch ein Bild zu ersetzen, wird dies zu einer Suchansicht erweitert, die die Liste während der Eingabe von Benutzernamen oder E-Mail filtert
- Dasselbe Template-Muster lässt sich nicht nur auf HTMX anwenden, sondern auch auf HTML-over-the-wire-Tools wie Unpoly und Hotwire
- Wer die Grundlagen von HTMX nicht kennt, sollte sich zuerst die offizielle Dokumentation ansehen
Projektverzeichnis und Aufbau statischer Dateien
- Das Projekt ist nach Rollen in folgende Bereiche unterteilt
assets/html/base.tmpl: HTML-Grundgerüst, das alle Seiten gemeinsam nutzen
assets/html/pages/: Inhalte einzelner Seiten
assets/html/partials/: HTML-Fragmente, die an mehreren Stellen wiederverwendet werden
assets/static/css, assets/static/img, assets/static/js: statische Assets
cmd/web/: Serverstart, Handler und HTML-Renderer-Code
- Das Grundgerüst wird mit folgenden Befehlen erstellt
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
- HTMX kann per CDN oder NPM installiert werden, meist wird jedoch eine Kopie heruntergeladen und direkt als statische Datei der Anwendung ausgeliefert
- Die Konfiguration ist einfach und Nachteile der CDN-Nutzung lassen sich vermeiden
- Verwendet werden
htmx.org@2.0.10 und das klassenlose CSS-Framework bamboo.css@1.4.0
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css
Trennung von Basis-, Seiten- und Partial-Templates
base.tmpl definiert das gesamte Dokumentgerüst wie <head>, gemeinsamen Titel und <main> und fügt page:title sowie page:content ein
- Bamboo CSS und das HTMX-Skript werden im Basis-Template geladen; beim HTMX-Skript wird das Attribut
defer gesetzt
- Der Browser lädt das Skript parallel, während er das HTML parst
- Nach Abschluss von HTML-Parsing und DOM-Aufbau wird das Skript ausgeführt
- Alle Templates erhalten unabhängig vom Dateinamen mit
{{define}}...{{end}} explizite Namen
- Go-Code kann Templates konsistent nur über die definierten Namen referenzieren
- Der
: in page:title ist ein beliebiges Trennzeichen; auch Namen wie page_title, page-title, pageTitle oder title sind möglich
- Für den Button auf der Startseite werden zwei Verhaltensweisen festgelegt
hx-get="/gopher": sendet beim Klick einen GET /gopher-Request
hx-swap="outerHTML": ersetzt den Button selbst durch das Antwort-HTML
partial:image:gopher ist ein wiederverwendbares Bildfragment; über width="{{.}}" wird beim Aufruf die Bildbreite übergeben
- HTML-Fragmente, die nur auf einer bestimmten Seite verwendet werden, können in der jeweiligen Seitendatei liegen, statt im gemeinsamen Verzeichnis
partials
- Fragmente, die auf mehreren Seiten genutzt werden, liegen im gemeinsamen Verzeichnis; seitenspezifische Fragmente werden neben dem zugehörigen Inhalt platziert
HTML und statische Assets in das Go-Binary einbetten
- Seit der Einführung von File-Embedding in Go 1.16 lassen sich HTML und statische Assets in das Go-Binary einbetten, statt sie zur Laufzeit von der Festplatte zu lesen
- Mit der Direktive
//go:embed "html" "static" werden die beiden Verzeichnisse in ein embed.FS aufgenommen und mit fs.Sub() in zwei Unter-Dateisysteme getrennt
HTMLFiles: Template-Dateisystem mit html als Root
StaticFiles: statisches Dateisystem mit static als Root
- Die Trennung hat zwei Vorteile
- HTML-Code und Code für statische Dateien greifen nicht unnötig auf die Dateien des jeweils anderen Bereichs zu
- Beim Öffnen von Dateien muss kein Präfix
html/ oder static/ angehängt werden
- Wer in
sub() kein panic() verwenden möchte, kann auch Fehler zurückgeben und die beiden Variablen in main() initialisieren
- Damit
fs.Sub() in der aktuellen Implementierung fehlschlägt, müsste der Verzeichniswert kein gültiger Pfad sein; die festen Strings "html" und "static" bestehen diese Prüfung, daher ist das Risiko einer Runtime-Panic sehr gering
Gemeinsame Template-Menge und htmlRenderer
htmlRenderer verwaltet den Rendering-Ablauf wie folgt
- Beim Start wird einmal eine gemeinsame Template-Menge geparst
- Bei jedem Rendering wird die gemeinsame Menge geklont
- Die für den Request benötigten Seiten-Templates werden zusätzlich geparst
- Das Template mit dem angegebenen Namen wird ausgeführt und als HTTP-Antwort gesendet
newHTMLRenderer() registriert eine template.FuncMap und parst anschließend die gemeinsamen Templates
now wird mit time.Now verbunden
- Weitere benutzerdefinierte Template-Funktionen können an derselben Stelle hinzugefügt werden
render() erstellt mit sharedTemplates.Clone() eine request-spezifische Template-Menge und erweitert sie bei vorhandenen additionalTemplateFiles mit ParseFS()
- Templates werden zunächst in einen
bytes.Buffer ausgeführt; anschließend wird der Statuscode geschrieben und der Antwort-Body gesendet
- Bei der Serverinitialisierung werden
"base.tmpl" und "partials/*.tmpl" als gemeinsame Menge geparst
- Das Basis-Template und alle gemeinsamen Fragmente stehen dem Renderer immer zur Verfügung
- Nur seitenspezifische Templates werden beim Handler-Aufruf ergänzt
- Statische Dateien werden über
http.FileServerFS(assets.StaticFiles) und den Pfad /static/ bereitgestellt; der Server läuft auf :5051
Ganze Seiten und HTML-Fragmente rendern
- Der Startseiten-Handler gibt mit folgendem Aufruf das gesamte Dokument zurück
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
- Dieser Aufruf fügt der gemeinsamen Menge
pages/home.tmpl hinzu und führt das Template base mit 200 OK aus
- Der Handler
/gopher parst keine zusätzlichen Dateien, sondern führt nur das bereits in der gemeinsamen Menge enthaltene partial:image:gopher aus
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
- HTMX ersetzt das von
/gopher erhaltene Bild gemäß der Einstellung hx-swap="outerHTML" durch den Button
- Diese Struktur bietet folgende Vorteile
- Templates und statische Assets sind in der Binary enthalten, was die Bereitstellung vereinfacht
- Mit demselben
render() können sowohl vollständige HTML-Seiten als auch bestimmte HTML-Fragmente zurückgegeben werden
- Basis-Template und Partial-Templates reduzieren doppelte Markup-Strukturen
- Partial-Templates können im Basis-Template, im Seiteninhalt und in anderen Partial-Templates wiederverwendet werden
Benutzersuche mit Aktualisierung während der Eingabe
- Die Suchfunktion ist in zwei Routen aufgeteilt
GET /users: eine vollständige HTML-Seite mit allen Benutzerinformationen
GET /users/search: ein HTML-Fragment, das nur die Tabellenzeilen der Benutzer zurückgibt, deren Name oder E-Mail zur Suchanfrage passt
- Das Sucheingabefeld kombiniert folgende HTMX-Attribute
- Mit
hx-get="/users/search" wird die Suchanfrage gesendet
- Mit
hx-trigger="input changed delay:500ms, keyup[key=='Enter']" wird die Anfrage ausgelöst, wenn nach einer Eingabeänderung 500 ms vergangen sind oder Enter gedrückt wird
- Der Suchbegriff wird als Query-String in der Form
GET /users/search?query=foo übergeben
- Mit
hx-target="#search-results" wird die Antwort in <tbody id="search-results"> eingefügt
- Mit
hx-push-url="true" wird bei jeder Anfrage die Adressleiste aktualisiert und ein Eintrag zur Browser-History hinzugefügt
users:rows ist ein separates Template, das nur Benutzerzeilen rendert
- Jede Zeile zeigt Name und E-Mail an
- Wenn
IsGopher wahr ist, wird partial:image:gopher mit Breite 24 gerendert
listUsers und searchUsers fügen beide pages/users.tmpl zur gemeinsamen Menge hinzu, führen aber unterschiedliche Templates aus
listUsers: führt base aus und gibt die vollständige Seite zurück
searchUsers: führt users:rows aus und gibt nur die passenden Zeilen zurück
- Ist der Suchbegriff leer, wird die vollständige Benutzerliste verwendet; ist ein Wert vorhanden, werden Name und E-Mail mit
strings.Contains() geprüft
- In einer echten Anwendung könnten folgende Verbesserungen erwogen werden
- Listen- und Such-Handler zusammenführen
- Fehlerbehandlung und Logging-Helfer zentralisieren
- Per Middleware Content-Security-Policy-Header und Panic-Recovery hinzufügen
- Server-Timeouts angemessen konfigurieren
Vollständige Seiten und Teilantworten mit HX-Request unterscheiden
- Wenn ein Nutzer
/users/search?query=leo direkt im Browser öffnet oder über einen geteilten Link besucht, ist möglicherweise nur das Teil-HTML für die Tabellenzeilen sichtbar
- HTMX-Anfragen enthalten immer den Header
HX-Request: true, sodass die Antwortform anhand dessen bestimmt wird
func isHTMXRequest(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
- Das Standard-Ausführungstemplate des Such-Handlers wird auf
base gesetzt und nur bei HTMX-Anfragen auf users:rows geändert
- Bei einem direkten Besuch erhält man eine vollständige Seite mit den Suchergebnissen
- Bei einer HTMX-Anfrage wird nur das Zeilenfragment zurückgegeben, das in
<tbody> eingefügt wird
Vary-Header und Wiederherstellung beim Zurückgehen
- Da dieselbe URL je nach
HX-Request-Wert unterschiedliche Antworten zurückgibt, muss auch der Cache über diesen Unterschied informiert werden
- Allen Rendering-Antworten wird
Vary: HX-Request hinzugefügt
- Streng genommen ist es weniger verschwenderisch, ihn nur in einzelnen Handlern zu setzen, wenn er nötig ist
- Wird er pauschal im Renderer gesetzt, lassen sich Bugs durch Vergessen vermeiden
- Wenn
hx-push-url oder hx-boost Browser-History-Einträge hinzufügen, cached HTMX das fertige Seiten-HTML im lokalen Speicher des Browsers
- Der Standard-Cache umfasst maximal 10 Seiten
- Wenn man zu einem Punkt zurücknavigiert, der nicht im Cache ist, fordert HTMX die entsprechende URL erneut vom Server an
- Die standardmäßige Wiederherstellungsanfrage enthält
HX-Request: true, sodass der Server statt einer vollständigen Seite Teil-HTML zurückgeben kann
- Wenn die Antwortart über
HX-Request ausgewählt wird, sollte historyRestoreAsHxRequest: false gesetzt werden
- Dadurch wird
HX-Request: true bei Wiederherstellungsanfragen nach einem Cache-Miss weggelassen
- Der Server gibt für diese Anfrage dann eine vollständige HTML-Seite zurück
- Die HTMX-Konfiguration wird im Meta-Tag
htmx-config in base.tmpl platziert
HTMX-Anfragen auf eine andere Seite umleiten
- HTMX kann HTML-Fragmente wie Erfolgsmeldungen sofort ersetzen und reduziert dadurch Fälle, in denen ein klassischer Post/Redirect/Get-Ablauf nötig ist
- Wenn man nach einem erfolgreichen Login wie etwa zum Profil auf eine komplett andere Seite wechseln muss, lässt sich das nicht allein mit einer normalen 3xx-Antwort behandeln
- Der Browser folgt der 3xx-Antwort, bevor HTMX sie verarbeitet
- HTMX sieht nur die finale Antwort nach der Weiterleitung und ersetzt deren Inhalt im bisherigen Ziel
- Für HTMX-Anfragen verwendet man eine 2xx-Antwort zusammen mit dem Header
HX-Redirect
HX-Redirect: /foo/bar navigiert den Browser zu dieser URL und lädt die gesamte Seite neu
- Die Anfrage an das Ziel enthält kein
HX-Request: true
- Wenn auch Umgebungen unterstützt werden sollen, in denen JavaScript deaktiviert ist oder HTMX nicht geladen wurde, sollten Nicht-HTMX-Anfragen weiterhin eine normale 3xx-Antwort erhalten
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
if isHTMXRequest(r) {
w.Header().Set("HX-Redirect", url)
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, url, code)
}
- Für eine Weiterleitung nach
/profile nach dem Login kann es so aufgerufen werden
redirect(w, r, "/profile", http.StatusSeeOther)
Unterschied zwischen HX-Redirect und HX-Location
HX-Location führt ein redirect-ähnliches Verhalten aus, ohne die ganze Seite neu zu laden
- Es holt das HTML der Ziel-URL
- Es ersetzt die Antwort in
<body>
- Es fügt der Browser-History einen neuen Eintrag hinzu
- Bei der Anfrage an die Ziel-URL wird
HX-Request: true mitgesendet
- Wenn die Zielroute je nach
HX-Request Teil-HTML zurückgibt, kann das zu Problemen führen
- Der Handler kann eine von
HX-Location kommende Anfrage nicht von einer normalen HTMX-Anfrage unterscheiden
- Für erstere wird eine vollständige Seite benötigt, für letztere eine Teilantwort
- Anders als bei der History-Wiederherstellung gibt es bei
HX-Location keine Einstellung, um HX-Request: true abzuschalten
- In den meisten Fällen ist es einfacher und sicherer, das vollständige Neuladen der Seite in Kauf zu nehmen und
HX-Redirect zu verwenden
- Wenn die Zielroute so konfiguriert ist, dass sie immer vollständiges HTML zurückgibt, kann auch
HX-Location verwendet werden
Verarbeitung von Antworten mit 204, 422, 4xx und 5xx
- HTMX ersetzt 4xx- oder 5xx-Antworten standardmäßig nicht im DOM
- Der aktuelle Bildschirm bleibt unverändert
- Der Fehler wird in der Konsole der Entwicklertools protokolliert
- Selbst wenn der Server wegen eines nicht vorhandenen Template-Namens
500 Internal Server Error zurückgibt, sieht der Nutzer den Fehler bei der Standardeinstellung nicht auf dem Bildschirm
- Um Fehlermeldungen dem Nutzer anzuzeigen, werden 4xx- und 5xx-Antworten so konfiguriert, dass sie den gesamten
<body> ersetzen
- Die Ausnahme ist
422 Unprocessable Content
- Dieser Status wird verwendet, wenn ein Formular mit Validierungsfehlern zurückgegeben wird
- Die Antwort sollte normal im bestehenden HTMX-Ziel ersetzt werden
responseHandling wird je nach Statuscode wie folgt konfiguriert
204 No Content: ändert das DOM nicht
422 Unprocessable Content: normal im bestehenden Ziel ersetzen
- alle übrigen 4xx und 5xx: Antwort in
<body> ersetzen
- sonstige Antworten: normal im bestehenden Ziel ersetzen
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
]
- Wenn bei einer bestimmten Interaktion Fehler in ein anderes Ziel als
<body> eingefügt werden sollen, kann die Standardeinstellung mit der Response-Target-Erweiterung überschrieben werden
Aktuelle URL in der Adressleiste des Browsers prüfen
- HTMX ändert die Browser-URL während AJAX-Anfragen und dem Ersetzen von Antworten nicht, sofern nicht
hx-boost, hx-push-url oder hx-replace-url verwendet werden
- Dadurch kann sich
r.URL im Go-Handler von der URL unterscheiden, die der Nutzer in der Adressleiste sieht
- HTMX übermittelt die aktuell im Browser angezeigte URL im Header
HX-Current-URL jeder Anfrage
- Eine Hilfsfunktion parst diesen Header als
url.URL und gibt, falls der Header fehlt, r.URL zurück
func browserURL(r *http.Request) (*url.URL, error) {
cu := r.Header.Get("HX-Current-URL")
if cu != "" {
return url.Parse(cu)
}
return r.URL, nil
}
Konfiguration zum Ändern der HTMX-Standardwerte
- Mit
historyCacheSize: 0 wird der Seitencache im lokalen Speicher von HTMX vollständig deaktiviert
- Der Cache im lokalen Speicher kann eine Quelle für Bugs und Sicherheitsprobleme sein
- Beim Zurücknavigieren wird das HTML erneut vom Server geladen, statt aus dem Cache zu kommen
- In künftigen HTMX-Versionen soll das Caching im lokalen Speicher aus demselben Grund standardmäßig deaktiviert werden
- Mit
disableInheritance: true wird die Vererbung von HTMX-Attributen deaktiviert
- Es ist klarer, Attribute immer explizit zu deklarieren
- Das senkt das Risiko von Bugs oder unbeabsichtigtem Verhalten
- In künftigen HTMX-Versionen soll auch die Attributvererbung standardmäßig deaktiviert werden
- Mit
includeIndicatorStyles: false wird verhindert, dass HTMX Indicator-Styles injiziert
- Die Indicator-Styles werden zusammen mit anderen CSS-Regeln selbst definiert, um Konsistenz zu wahren
- HTMX-Anfragen haben standardmäßig kein Timeout und warten, bis der Server antwortet
- Je nachdem, wie die Go-Anwendung Timeouts und Deadlines behandelt, kann
timeout in Millisekunden angegeben werden
- Als Ausgangspunkt wird
timeout: 5000 verwendet
- Die Basiskonfiguration enthält außerdem
historyRestoreAsHxRequest: false sowie responseHandling je nach Statuscode
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"historyCacheSize": 0,
"historyRestoreAsHxRequest": false,
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
],
"timeout": 5000
}'
>
Seitenspezifische Layouts für größere Anwendungen
- Wenn ein Basis-Template und seitenspezifische Templates nicht ausreichen, kann zwischen diesen beiden Ebenen ein Layout-Template eingefügt werden
- Das eignet sich für Bereiche wie Admin-Seiten, die einen anderen Titel, ein anderes Navigationsmenü oder eine andere Inhaltsstruktur benötigen als normale Seiten
base ruft nicht direkt page:content auf, sondern layout
<body>
{{template "layout" .}}
</body>
layouts/admin.tmpl definiert die gemeinsame Struktur des Admin-Bereichs und ruft darin page:content auf
- Titel des Admin-Bereichs
- Navigationsmenü zu Nutzern und Bestellungen
- Ein
<main> für seitenspezifische Inhalte
- Der Handler übergibt das zu verwendende Layout und das Seiten-Template gemeinsam an
render()
app.html.render(
w,
200,
nil,
"base",
"layouts/admin.tmpl",
"pages/admin-orders.tmpl",
)
- So lassen sich bei beibehaltener gemeinsamer Basisvorlage routenspezifische Layouts und Seiteninhalte kombinieren; auch Ansichten, die wie der Admin-Bereich eine eigene Struktur benötigen, können mit demselben Rendering-Muster erweitert werden
1 Kommentare
Meinungen auf Hacker News
Man mag Go + HTMX und nutzt es zusammen mit a-h/templ, um die Typsicherheit von Templates, Komponenten und partiellem HTML zu erhöhen. Die gesamte Toolchain aus Go, Unix und SQLite wurde als GUS-Stack veröffentlicht: https://housecat.com/blog/the-gus-stack-go-unix-sqlite Er ist stark vom GUTS-Stack von exe.dev inspiriert, verwendet aber HTMX statt TypeScript: https://exe.dev/docs/guts Für Stacktrace-Fehler kommt cockroachdb/errors zum Einsatz, für typsicheres HTML templ, für die Generierung von OpenAPI-Spezifikationen fuego, für SQL-Codegenerierung sqlc, für reines Go-SQLite modernc.org/sqlite, für Migrationen goose, für Persistenz-Workflows dbos und für Chrome/CDP-Tests sowie Automatisierung rod. Diese Kombination ist sowohl beim direkten Programmieren als auch mit Unterstützung durch Agenten produktiv, ebenso beim Bauen und Deployen eines einzelnen Binaries.
go generate ./...als Build-Schritt akzeptiert, gewinnt man viel; mit goverter werden außerdem Konvertierungen zwischen sqlc-Modellen und Template-Objekten bzw. Rückgabewerten generiert. Etwa 50 % des Boilerplates werden automatisch erzeugt, wodurch die Typsicherheit sehr gut wird.HTMX ist für viele Einsatzzwecke großartig, lässt sich aber sehr schwer einführen, wenn die Teamkollegen nicht mitziehen. Es gibt anhaltenden Widerstand nach dem Motto, das sei keine ernsthafte Technologie, und verschiedenste Bugs werden zunächst HTMX angelastet; selbst wenn sich das später als Missverständnis herausstellt, ist das Vertrauen bereits beschädigt. Das passierte sogar im besten Team, in dem ich bisher gearbeitet habe, und am Ende habe ich gelernt, dass man sich seine Kämpfe aussuchen muss. Auch Gos html/template hat aus meiner Sicht eine ungewöhnlich unnatürliche Schnittstelle. Wie 《A Philosophy of Software Design》 empfiehlt, sollte Komplexität nach innen verlagert werden; eine Struktur, bei der man sich bei jedem Rendern von HTML um das Klonen von Templates kümmern muss, ist unnötig kompliziert.
base.tmplinBASE_BEGINundBASE_ENDaufteilen, die dann im finalen Template verwendet werden. Der Seitentitel muss zwar zur Laufzeit übergeben werden, aber die Vorgehensweise aus dem Original bricht ohnehin zusammen, sobald man mit Mehrsprachigkeit beginnt.In einem aktuellen Projekt habe ich HTMX mit großer Freude eingesetzt. Als jemand, der das Web vor AngularJS und React kennt, gefällt mir besonders, dass man echte Seiten bauen und die Menge an JavaScript minimieren kann. Mit reinem JavaScript wäre das ebenfalls möglich, aber HTMX nimmt einem den wiederkehrenden Boilerplate für Event-Handling ab. Wenn man die moderne Frontend-Philosophie nicht mag, ist es einen Versuch wert; die ersten Beispiele auf der offiziellen Website sind sehr grundlegend, aber wenn man etwas tiefer einsteigt, ist es deutlich mächtiger.
Ich habe ein ziemlich großes Projekt mit HTMX + Go gebaut, aber es war noch nicht ausreichend, und ich bin mir nicht sicher, ob es künftig das gewünschte Niveau erreichen wird. Für einfache CRUD-Apps und Admin-Dashboards ist es großartig, aber sobald es viele miteinander verbundene Komponenten, geteilten State und komplexe Interaktionen gibt, wird die Verwaltung schnell schwierig. Weil ich React nicht mochte, hatte ich HTMX gewählt, bin am Ende aber beim Backend bei Go geblieben und habe das Frontend auf SvelteKit im SPA-Modus umgestellt. Durch die saubere Trennung beider Seiten ließ sich die komplexe UI deutlich leichter entwickeln und warten. Svelte fühlt sich eher wie eine natürliche Erweiterung von HTML an als wie eine andere Sprache namens JSX; State-Management und Komponentenmodell sind einfach, und die neue
$state-Syntax gefällt mir besonders gut.Heutzutage schreibe ich Nebenprojekte fast alle mit Go + HTMX, unabhängig davon, ob ich dabei Hilfe von einem LLM bekomme. Auch Opus und GPT kommen mit dieser Kombination sehr gut zurecht; man kann schnell bauen und loslegen, und weil es ein einzelnes Binary ist, sind Deployment und Hosting bequem. Für schnelle Iterationen ist das ein sehr guter Stack.
Wenn man HTMX verwendet, empfehle ich dringend eine Art der HTML-Erzeugung, mit der sich auch im Backend gemeinsame HTML-Fragmente wie in React leicht in Funktionen auslagern und komponentisieren lassen. HTMX braucht je nach Situation Flexibilität beim vom Backend erzeugten HTML, etwa weil man Tags direkt neben bestimmtem HTML hinzufügen oder entfernen muss. Wenn es zum Beispiel auf oberster Ebene ein ``-Tag findet, aktualisiert es den Seitentitel. Mit traditionellen stringbasierten Template-Engines ist so etwas schwierig, mit sprachintegrierten HTML-Bibliotheken dagegen einfach; außerdem kann man die Abstraktionsmöglichkeiten der ganzen Sprache nutzen, was besonders gut zu HTMX passt. Bei einem JavaScript-Backend kann man eine Tagged-Template-Literal-Funktion wie https://github.com/WebReflection/uhtml-ssr verwenden, bei Go https://www.gomponents.com/ und bei Scala ScalaTags.
renderToStaticMarkup, aber wahrscheinlich ist es besser, eine speziell fürs Backend geschriebene JSX-Bibliothek zu wählen.Mit den Büchern von Alex Edwards und Learn Go with Tests bin ich zum ersten Mal mit Go in Berührung gekommen, und ich empfehle sie immer noch. Jetzt bekomme ich Lust, einige ältere Projekte auf HTMX umzustellen.
Ich habe für den Eigenbedarf ein Framework auf Basis von Kotlin + HTMX gebaut: https://github.com/reubenfirmin/zoned Ziel war zu prüfen, ob sich eine Web-App durchgängig typsicher machen lässt; für HTML verwende ich Kotlinx.html, das HTML in einer JSX-ähnlichen DSL schreibt. Die ersten 90 % habe ich selbst gebaut, für die letzten 10 % und das README habe ich Claude hinzugezogen, um die Veröffentlichung abzuschließen.
Ich mag sowohl Go als auch Alex Edwards sehr, aber die Ergebnisse mit HTMX waren für mich immer enttäuschend. Es fühlt sich an, als wachse die Komplexität der Codebase doppelt so schnell wie die der App selbst, und man stößt ständig auf Randfälle, aus denen man ohne merkwürdige Workarounds nicht herauskommt. Ich verstehe, warum man Node-Pakete nicht mag, aber HTMX wirkt wie eine Überkompensation dafür. Die Zeit, die man dadurch spart, nicht mit JSON zu ringen, gibt man dreifach dafür aus, Aussehen und Bediengefühl der App richtig hinzubekommen. Das Mantine-Template ist in zwei Minuten eingerichtet und bietet hervorragende UI-Komponenten; bindet man die gebauten statischen Assets ein, kann man daraus genauso ein einzelnes Go-Binary machen: https://github.com/mantinedev/vite-min-template
Go + Datastar ist einfacher, deshalb bevorzuge ich es deutlich.