//

Hotwire: Ein neuer (alter) Ansatz für moderne Webanwendungen

24.8.2022 | 10 Minuten Lesezeit

Hotwire (HTML over the wire) wurde Ende 2020 von Basecamp vorgestellt und verspricht einen alternativen Ansatz zur Entwicklung moderner Webanwendungen mit weniger JavaScript:

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire. [1]

Über meine ersten Erfahrungen mit Hotwire berichte ich in diesem Artikel, der auch auf Englisch verfügbar ist .

Mein persönlicher Eindruck aus den letzten Jahren ist, dass mit modernen Webanwendungen folgender Architekturansatz gleichzusetzen ist:

  • API-first-Ansatz auf Serverseite
  • Single-Page Application (SPA) auf Clientseite, das aus JSON-Antworten vom Backend und JavaScript clientseitig HTML erzeugt (egal ob Angular, Vue.js oder React, …)

Eines der häufigsten Argumente für den Einsatz dieser Architektur und gegen den Einsatz von Server-Side Rendering (SSR), also der Erzeugung von HTML auf Serverseite, ist die vermeintlich bessere User Experience bei SPAs: SPA-Frameworks laden die Inhalte einer Webanwendung dynamisch. Unter der Haube wird dafür aber auch nur ein JavaScript-API benutzt – und dieses API ist nicht nur von SPAs benutzbar! Das Thema Benutzbarkeit ist aber auch ein großes Risiko beim Einsatz von SPAs: Die zum Teil sehr großen JavaScript-Bibliotheken, die im Browser geladen werden müssen, können unter anderem auf Mobilgeräten bei schlechtem Empfang zu sehr langen Ladezeiten und zur Unbenutzbarkeit führen. Neben diesem Umstand verschwimmt beim Einsatz dieses Architekturansatzes die Trennung der Verantwortlichkeiten bei den Themen Aussehen und Verhalten, Präsentationslogik und Templating, Routing, Geschäftslogik sowie die Haltung des Zustands. [2] Wenn die SPA offline verfügbar sein soll, müssen alle genannten Themen (zumindest zum Teil) im Client umgesetzt sein. Bei „klassischen“ Architekturen mit SSR ist lediglich das Aussehen und Verhalten im Client verortet, alles andere übernimmt das Backend. Offline-Fähigkeit ist übrigens ein guter Grund für den Einsatz einer SPA. 

Viele Anwendungen, die ich bisher kennenlernen durfte, hätten aber keine SPA auf der Clientseite benötigt und dennoch wurden sie mit Angular oder React entwickelt. Denkt mal kurz an eure letzten Projekte: Habt ihr euch überlegt, ob ihr eine SPA einsetzen wollt oder seid ihr direkt zur Frage nach dem Framework gesprungen?

Alle Code-Beispiele, die in diesem Artikel besprochen werden, findet ihr auf GitHub inklusive eines lauffähigen Beispiel-Projekts um die Code-Schnipsel herum.

Hotwire

Hotwire ist eine Sammlung von JavaScript-Modulen, die das Verhalten einer SPA (z. B. dynamisches Laden) für klassische Architekturen mit SSR herstellt. Hotwire setzt dafür auf einen HTML-first-Ansatz, sodass das Backend nicht mehr JSON, sondern HTML zurückgibt.

Hotwire besteht aus drei Bausteinen, die sich gut kombinieren lassen sich aber nicht gegenseitig voraussetzen:

  • Turbo: Turbo ist das Herz von Hotwire und der Fokus dieses Artikels. Turbo Drive ersetzt die Navigation in klassischen Webanwendungen, sodass ein Verhalten analog zu SPAs ohne komplettes Neuladen der Seite beim Navigieren erzeugt wird. Mittels Turbo Frames lassen sich Seiten in kleinere Blöcke aufteilen, die sich unabhängig voneinander aktualisieren lassen. Turbo Streams ermöglichen Streaming Updates der Inhalte. Turbo übernimmt also bereits vieles von dem, was Angular, Vue.js, React etc. sonst tun.
  • Stimulus: Mit Stimulus können sogenannte Controller entwickelt werden, die die Funktionalität eines Custom Elements bieten. Dabei werden HTML-Elemente in einem HTML-first-Ansatz um JavaScript Logik angereichert (z. B. kann an einen Button die Logik gebunden werden, sich mit einer Server-Sent Events EventSource zu verbinden).
  • Strada: Strada ist noch nicht veröffentlicht, aber soll einen Weg bieten, nativen Code und Web Views in mobilen Apps interagieren zu lassen.

Quarkus und Qute mit Hotwire

Hotwire verspricht, plattformunabhängig zu funktionieren es also egal ist, mit welcher Technologie das Backend realisiert ist. Viele Beispiele, die ich im Netz zu Hotwire finden konnte, sind entweder mit Ruby oder mit Spring Boot (in Kombination mit Thymeleaf für Templating) im Backend umgesetzt. Ich bin ein großer Fan von Quarkus als Alternative zu Spring Boot und möchte es daher einsetzen. Quarkus bringt mit Qute seine eigene Templating Engine mit. Quarkus und Qute werde ich in diesem Artikel nicht näher erläutern für das Verständnis darüber, wie Hotwire funktioniert, braucht es aber kein tiefergehendes Know-how über Quarkus und Qute, da die gesamte Funktionalität Framework-unabhängig durch Auszeichnungen im HTML erreicht wird.

Die Code-Beispiele für diesen Artikel stammen aus einer Beispielanwendung: Es handelt sich um eine simple To-do-App mit In-Memory-Datenhaltung. Über die App können To-dos angezeigt, hinzugefügt, als erledigt markiert und entfernt werden.

Turbo

Turbo ist das Herz von Hotwire und übernimmt eine ganze Reihe an Funktionen, die nachfolgend im Detail beschrieben werden.

Turbo Drive: Navigation ohne Neuladen

Eine nahtlose Navigation ohne Neuladen und mit fließenden Übergängen, wie man sie aus SPAs gewohnt ist, ist am einfachsten umzusetzen. Man muss lediglich Turbo in die Applikation einbinden:

1<script crossorigin="anonymous" src="https://unpkg.com/@hotwired/turbo@7.1.0/dist/turbo.es2017-umd.js"></script>
2

Ich habe Turbo über unpkg innerhalb eines <script>-Tags eingebunden. Turbo Drive übernimmt dann automatisch die Navigation bei Klick auf Links oder Formularübermittlungen, die innerhalb der gleichen Domain (Protokoll/Host/Port-Kombination) bleiben. Im Hintergrund werden die Link-Klicks und Formularübermittlungen als XHR ausgeführt und die Browser-Historie wird angepasst, sodass die Vorwärts- bzw. Rückwärtsnavigation über den Browser weiterhin funktioniert. Sobald die Antwort auf einen Request eintrifft, wird der Seiteninhalt, der momentan angezeigt wird, angepasst: Die Elemente aus <head> werden zusammengeführt, <body> wird durch den Empfangenen ersetzt. Wenn die Antwortzeit länger als 500 ms dauert, blendet Turbo Drive automatisch einen Fortschrittsbalken ein, sodass für die Benutzer*innen ersichtlich ist, dass die Seite lädt. Um eine noch bessere User Experience zu erzielen, versucht Turbo Drive, die neu anzuzeigende Seite aus dem Cache zu laden und nach Eintreffen der Antwort zu aktualisieren.
Turbo Drive lässt sich durch Attribute und zusätzliches JavaScript noch detaillierter kontrollieren: Dazu werft ihr am besten einen Blick ins Handbuch . Für die Beispielanwendung habe ich die Standardeinstellungen ohne weitere Konfiguration verwendet.

Turbo Frames: Frames mit gewissen Vorzügen

Mit Turbo Frames kann man Seiten in einzelne Blöcke unterteilen und dadurch noch besser kontrollieren, auf welche Teile der Seite sich eine Interaktion bezieht. Als Beispiel dient eine Liste an To-dos, die innerhalb einer Tabelle angezeigt werden sollen:

1<turbo-frame id="todolist">
2   <table>
3       <thead>
4           <tr>
5               <th>Title</th>
6               <th>Completed</th>
7               <th>Mark as done</th>
8               <th>Remove</th>
9           </tr>
10       </thead>
11       <tbody>
12           <tr>
13               <td><a href="/todos/123">Todo</a></td>
14               <td>Open</td>
15               <td>
16                   <form action="/todos/123" method="POST" enctype="multipart/form-data">
17                       <input type="submit" value="Done">
18                   </form>
19               </td>
20               <td>
21                   <form action="todos/remove/123" method="POST" enctype="multipart/form-data">
22                       <input type="submit" value="Remove">
23                   </form>
24               </td>
25           </tr>
26       </tbody>
27   </table>
28</turbo-frame>
29

Um einen Teil der Seite als Turbo Frame für Turbo zu kennzeichnen, wird das Tag <turbo-frame> verwendet. Um die Frames identifizieren zu können, gibt man diesen eine ID (todolist). Turbo sorgt dann dafür, dass sich Interaktionen innerhalb des Frames (hier zum Beispiel die Interaktion „Als erledigt markieren“) nur auf diesen beziehen. Wenn als Antwort auf einen Request ein Turbo Frame mit der gleichen ID zurückkommt, wird dieser Block ausgetauscht und der Rest der Seite bleibt, wie man es von einer SPA kennt, unangetastet.

1<turbo-frame id="todolist">
2   <table>
3       <thead>
4           <tr>
5               <th>Title</th>
6               <th>Completed</th>
7               <th>Mark as done</th>
8               <th>Remove</th>
9           </tr>
10       </thead>
11       <tbody>
12           <tr>
13               <td><a href="/todos/123">Todo</a></td>
14               <td>Done</td>
15               <td>
16                   <form action="/todos/123" method="POST" enctype="multipart/form-data">
17                       <input type="submit" value="Done" disabled>
18                   </form>
19               </td>
20               <td>
21                   <form action="todos/remove/123" method="POST" enctype="multipart/form-data">
22                       <input type="submit" value="Remove">
23                   </form>
24               </td>
25           </tr>
26       </tbody>
27   </table>
28</turbo-frame>
29

Lazy Loading geschenkt!

Als besonderes Schmankerl gibt es Lazy Loading von Turbo geschenkt. Beim Laden einer Seite kann man einzelne Abschnitte mit einem Platzhalter ausliefern (hier ein leeres <div>, das per CSS einen Spinner anzeigt), wodurch die Seite schneller geladen wird und bereits Elemente angezeigt werden, die keine lange Ladezeit haben.

1<h1>TODO List</h1>
2<turbo-frame id="todolist" src="/todos">
3   <div class="loader"></div>
4</turbo-frame>
5

Das src-Attribut teilt Turbo mit, dass ein Request ausgelöst werden soll. Die Antwort auf den Request wird so behandelt, wie wir es bereits kennengelernt haben: Wenn die Antwort einen Turbo Frame mit der gleichen ID enthält, wird dieser Block ersetzt.

Frames von außerhalb aktualisieren?

Das geht! Dafür wird das Attribut data-turbo-frame benutzt, das Turbo mitteilt, welcher Frame aktualisiert werden soll.

1<h1>TODO List</h1>
2<turbo-frame id="todolist" src="/todos">
3   <div class="loader"></div>
4</turbo-frame>
5
6<h1>Create TODO</h1>
7<form action="/todos" method="POST" enctype="multipart/form-data" data-turbo-frame="todolist">
8   <label for="name">TODO:</label>
9   <input type="text" id="name" name="name" required>
10   <br>
11   <input type="submit" value="Create">
12</form>
13

Wenn als Antwort auf den POST Request zum Erstellen eines To-dos ein <turbo-frame> mit der ID todolist kommt, wird dieser Frame aktualisiert.
Während meiner Implementierung bin ich in das Fettnäpfchen getreten, dass die Aktualisierung von Außen mit data-turbo-frame nicht funktioniert, wenn um den <turbo-frame> noch ein <div> mit der gleichen ID ist. Folgendes Beispiel funktioniert also nicht:

1<div id="todolist">
2   <h1>TODO List</h1>
3   <turbo-frame id="todolist" src="/todos">
4       <div class="loader"></div>
5   </turbo-frame>
6</div>
7
8<h1>Create TODO</h1>
9<form action="/todos" method="POST" enctype="multipart/form-data" data-turbo-frame="todolist">
10   <label for="name">TODO:</label>
11   <input type="text" id="name" name="name" required>
12   <br>
13   <input type="submit" value="Create">
14</form>
15

Turbo Streams: noch mehr Dynamik

Mit Turbo Streams können innerhalb einer Antwort auf einen Request mehrere Aktionen für eine Website geschickt werden. Zum Beispiel kann die Antwort auf einen Request zum Erstellen eines To-dos so aussehen:

1<turbo-stream action="append" target="todolistTable">
2   <template>
3       <tr id="123-row">
4           <td><a href="/todos/123">Todo</a></td>
5           <td>Open</td>
6           <td>
7               <form action="/todos/stream/123" method="POST" enctype="multipart/form-data">
8                   <input type="submit" value="Done">
9               </form>
10           </td>
11           <td>
12               <form action="/todos/stream/remove/123" method="POST" enctype="multipart/form-data">
13                   <input type="submit" value="Remove">
14               </form>
15           </td>
16       </tr>
17   </template>
18</turbo-stream>
19
20
21
22<turbo-stream action="remove" target="no-todos">
23
24</turbo-stream>
25

Turbo Streams definieren ein bestimmtes Format für die Response: Jede Aktion, die auf der Website ausgeführt werden soll, ist von einem <turbo-stream>-Element umschlossen. Im action-Attribut wird die Aktion angegeben, die ausgeführt werden soll. Die ID des HTML-Elements, das mit der Aktion adressiert werden soll, wird im target-Attribut definiert. Das muss in diesem Fall kein Turbo Frame sein, sondern kann jedes beliebige HTML-Element sein. Im Beispiel ist todolistTable einfach ein tbody-Element: <tbody id="todolistTable"> und no-todos eine Reihe in einer Tabelle: <tr id="no-todos">
Es gibt derzeit sieben Aktionen, die von Turbo Streams unterstützt werden: append, prepend, replace, update, remove, after, before. Die Aktionen machen genau das, was man von ihnen erwarten würde. In unserem Beispiel wird eine Zeile mit den Informationen zu einem To-do an todolistTable angehängt und außerdem die Reihe mit der ID no-todos aus der Tabelle entfernt. Wenn neuer Inhalt hinzugefügt oder bestehender Inhalt aktualisiert oder ersetzt werden soll, muss dieser innerhalb des Turbo Stream in einem <template>-Element angegeben werden.
Es können beliebig viele <turbo-stream>-Elemente innerhalb einer Antwort kombiniert werden. Wichtig ist aber, dass als Content Type text/vnd.turbo-stream.html benutzt wird. Den Rest erledigt dann Turbo. In der Beispielimplementierung der To-do-App mit Turbo Streams wird nun nicht mehr der entsprechende Turbo Frame aktualisiert, sondern die neue Reihe für das To-do in die Tabelle hinein gestreamt:


Alternativ zu Turbo Streams als Antworten auf Benutzerinteraktionen wie Formularübermittlungen können Turbo Streams auch durch Event-Streams übermittelt werden. So ein Stream muss explizit über ein JavaScript-API bei Turbo registriert werden (schreiben wir gerade zum ersten Mal JavaScript?), aber dafür muss der Content Type bei der Event-Übermittlung nicht mehr gesetzt werden. Turbo geht davon aus, dass die Events als Turbo-Stream-Aktionen interpretierbar sind. In der Beispielanwendung werden Server-Sent Events (SSE) als Event-Stream benutzt. Man kann aber genauso gut WebSockets einsetzen.

1<script type="text/javascript">
2   if (window["EventSource"] && window["Turbo"]) {
3       Turbo.connectStreamSource(new EventSource("/todos/sse/connect"));
4   } else {
5       console.warn("Turbo Streams over SSE not available");
6   }
7</script>
8

Dieser kleine JavaScript-Abschnitt reicht aus, um die EventSource bei Turbo zu registrieren. Auf Backend-Seite gibt es keine weiteren speziellen Anforderungen es handelt sich um ganz normale SSE- bzw. WebSocket-Verbindungen. Nur das Format der Events muss dem vorgegebenen Format von Turbo Streams entsprechen. Zu Demozwecken wird hier ein neues To-do von außerhalb des Clients angelegt und das Update erscheint auf der Website:

Schlussgedanken

Moderne Webanwendungen ohne SPA? Das geht! Und zwar dank Hotwire. Das Team von Basecamp hat hervorragende Arbeit geleistet.

Durch den Architekturansatz des SSR ist das Templating wieder an einer Stelle zentralisiert (so wie früher bei klassischen Webanwendungen). Außerdem entfällt dank der leichtgewichtigen Bibliothek Turbo das Warten darauf, dass die SPA wieder neu gebaut und die mehreren MB neu im Browser geladen wurden. Alles in allem eine runde Sache.

Mir persönlich hat die Entwicklung mit Hotwire viel Spaß gemacht. Dadurch, dass ein Großteil der Konfiguration bereits im HTML stattfindet, braucht man auch nicht tief in ein JavaScript-API einzusteigen. Erstaunt war ich besonders von Turbo Streams: Ohne viel JavaScript schreiben zu müssen, kann man ganz leicht Updates in die Website streamen.

Etwas mehr JavaScript braucht man bei Stimulus, einem weiteren Modul von Hotwire. Stimulus simuliert ganz vereinfacht gesprochen Custom Elements, aber wieder mit dem klaren Fokus auf HTML first. Ich habe Stimulus bewusst nicht näher beschrieben, weil ich persönlich direkt auf Custom Elements setzen würde, um möglichst nah an der Webplattform zu bleiben. Stimulus ist in einem eigenen Handbuch gut dokumentiert.

Die Einbindung von Hotwire in eine Quarkus-Anwendung mit Qute hat ohne Probleme funktioniert. Alles andere hätte mich auch überrascht, da es vor allem darum geht, die richtigen HTML-Elemente zu benutzen und die richtigen Attribute zu setzen.

Zum Abschluss kann ich nur empfehlen, selbst Erfahrungen mit Hotwire (und insbesondere Turbo) zu sammeln. Und vielleicht passt Hotwire ja für das nächste Projekt? Es muss nicht immer eine SPA sein.

Referenzen

[1] HTML Over The Wire | Hotwire: https://hotwired.dev/ (zuletzt aufgerufen am 24.08.2022)
[2] JavaScript? Gern, aber bitte in Maßen: https://www.innoq.com/de/articles/2019/11/javascript-in-ma%C3%9Fen/ (zuletzt aufgerufen am 24.08.2022)

Beitrag teilen

Gefällt mir

2

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.