Video showcasing the realtime aspect of the chatting application.
Dieser Blogartikel wurde inspiriert von einem Hotwire Turbo Streams Tutorial für Sinatra.
Wenn Sie nach Hotwire-Tutorials auf Google suchen, werden Sie feststellen, dass die meisten Ergebnisse mit Ruby on Rails in Verbindung stehen. Selbst die Hotwire-Anleitungen verwenden überwiegend Ruby on Rails als Beispiel für die Implementierung von Turbo Streams.
Das ist nicht wirklich überraschend, da das Framework vom selben Unternehmen entwickelt wurde, das auch hinter dem Ruby on Rails-Framework steht.
Es ist jedoch wichtig zu erkennen, dass Hotwire nicht ausschliesslich ein Rails-Framework ist. In diesem Blogartikel möchte ich Sie davon überzeugen, dass Hotwire über den Rails-Kontext hinaus verwendet werden kann, insbesondere Turbo, seine Kernfunktion.
Ziele
In diesem Blogartikel werde ich Folgendes erläutern:
Einrichten einer BunJS-Anwendung
Implementierung von Client- und Server-seitigen WebSockets
Verwendung von Turbo Streams zur Aktualisierung der Benutzeroberfläche
Erstellen eines Stimulus-Controllers, der an das DOM angehängt ist
Schritt 1: Einrichten einer BunJS-Anwendung
Ursprünglich habe ich erwogen, für diesen Blog Java Spring zu verwenden, bin jedoch auf Herausforderungen mit WebSockets gestossen. Stattdessen habe ich mich für eine wesentlich einfachere TypeScript-Anwendung entschieden.
Lassen Sie uns damit beginnen, eine neue BunJS-Anwendung zu initialisieren:
mkdir bunjs-turbo-demo
cd bunjs-turbo-demo
bun init
Nun, lassen Sie uns die Anwendung ausführen:
bun run index.ts
Hello via Bun!
Schritt 1: Implementierung eines einfachen Websocket HTTP-Servers mit Bun
WebSockets dienen als Grundlage für die Echtzeitkommunikation in unserem Projekt. In diesem Schritt werden wir einen grundlegenden Websocket HTTP-Server mit Bun erstellen. WebSockets sind unerlässlich, um die bidirektionale Kommunikation zwischen Clients (wie Webbrowsern) und dem Server zu ermöglichen.
Das Muster des Mehrfachverlegers - Mehrfachabonnenten
Unser Ansatz beinhaltet die Implementierung des Musters des Mehrfachverlegers - Mehrfachabonnenten. So funktioniert es:
Interaktion des Clients:
Clients (Webbrowser der Benutzer) initiieren Aktionen wie das Senden von Chatnachrichten oder das Anfordern von Aktualisierungen.
Diese Interaktionen lösen WebSocket-Verbindungen zum Server aus.
Serververarbeitung:
Der Server empfängt Nachrichten von mehreren Clients.
Er verarbeitet diese Nachrichten und bereitet entsprechende Antworten vor.
Aktualisierungen übertragen:
Wenn ein Client eine Nachricht sendet (z. B. eine neue Chatnachricht), überträgt der Server sie an alle verbundenen Clients.
Dies stellt sicher, dass jeder die neuesten Updates in Echtzeit erhält.
Nahtlose Kommunikation:
WebSockets ermöglichen eine nahtlose, latenzarme Kommunikation.
Clients können Updates sofort empfangen, ohne manuell die Seite aktualisieren zu müssen.
Hier ist ein einfaches Sequenzdiagramm, das die Kernidee dieses Projekts zeigt:
Implementierung des Servers
Aus Gründen der Kürze habe ich den Code für View-Helfer wie layoutHTML
und chatRoomHTML
weggelassen. Diese Helfer sind für das Rendern von HTML-Komponenten und Chat-Raumlayouts verantwortlich. Obwohl sie wichtig sind, beeinflussen ihre Details die Kernkonzepte, die in diesem Blog diskutiert werden, nicht wesentlich.
const topic = "chatroom";
Bun.serve({
port: 8080,
fetch(req, server) {
const url = new URL(req.url)
if (url.pathname === "/") return new Response(layoutHTML("Chatroom", chatRoomHTML()), {
headers: {
"Content-Type": "text/html",
}
})
if (url.pathname === "/subscribe") {
if (server.upgrade(req)) { return }
return new Response("Couldn't upgrade to a WebSocket connection")
}
return new Response("404!");
},
websocket: {
open(ws) {
console.log("Websocket opened")
ws.subscribe(topic)
ws.publishText(topic, messageHTML("Someone joined the chat"))
},
message(ws, message) {
console.log("Websocket received: ", message)
ws.publishText(topic, messageHTML(`Anonymous: ${message}`))
},
close(ws) {
console.log("Websocket closed")
ws.publishText(topic, messageHTML("Someone left the chat"))
},
publishToSelf: true
}
})
Implementieren des Clients
Die Anwendung ist ohne den Client unvollständig, der sich mit dem Websocket-Server verbindet. Gemäss der Dokumentation zur Verbindung mit dem Websocket-Server ist die Verbindung zum Backend unkompliziert:
const client = new WebSocket("ws://localhost:8080/subscribe")
const form = document.getElementById("chat-form")
const chatFeed = document.getElementById("chat-feed")
client.addEventListener("message", (event) => {
chatFeed.innerHTML += event.data
})
form.addEventListener("submit", (event) => {
event.preventDefault()
const formData = new FormData(form)
const message = formData.get("message")
client.send(message)
form.reset()
})
Vergessen Sie nicht, die Route für die JavaScript-Datei im Backend hinzuzufügen und sie in Ihrer Ansicht mit einem Modul-Skript-Tag einzubinden. Stellen Sie ausserdem sicher, dass Sie die Backend-Antwort implementieren.
Das Problem
Wie Sie hier sehen können, muss jede Änderung in der Benutzeroberfläche manuell implementiert werden. Im Moment hören wir auf ein einzelnes Ereignis und aktualisieren ein einzelnes Element. Wenn die Anwendung komplexer wird, wächst auch der JavaScript-Code. Was wäre, wenn ich Ihnen sagen würde, dass wir den Client-Code "fast" vollständig eliminieren können, indem wir Turbo Streams einführen?
Schritt 3: Implementieren von Turbo Streams
Turbo ist ein wesentlicher Bestandteil des Hotwire-Frameworks. Es ermöglicht Ihnen, den Bedarf an individuellem JavaScript erheblich zu reduzieren.
Das für diese Anwendung relevanteste Feature sind die Turbo Streams. Sie ermöglichen es uns, Seitenänderungen in Form von HTML über die Leitung zu übertragen.
Das Importieren von Turbo ist so einfach wie das Hinzufügen dieses Code-Snippets in unsere Layout-Datei:
<script type="module">
import hotwiredTurbo from 'https://cdn.jsdelivr.net/npm/@hotwired/[email protected]/+esm'
</script>
Um Websockets für Turbo Streams im Frontend zu verwenden, können wir das folgende Snippet aus der Turbo-Stream-Dokumentation verwenden:
<turbo-stream-source src="ws://localhost:8080/subscribe" />
Um die Benutzeroberfläche zu aktualisieren, wenn eine Nachricht gesendet wird, übertragen wir das folgende HTML über Websockets:
<turbo-stream action="append" target="chat-feed">
<template>
<p class="notice">
Anonymous: Hello World!
</p>
</template>
</turbo-stream>
Diese Methode der HTML-Aktualisierung zeichnet sich durch ihre Transparenz und Einfachheit aus. Zunächst wählen wir ein Element mit dem Selektor #chat-feed
aus und fügen dann den Inhalt der übertragenen Vorlage hinzu. In diesem Fall handelt es sich um einen Absatz mit der Benutzernachricht. Dadurch wird fast der gesamte clientseitige JavaScript-Code für die Seitenaktualisierung eliminiert.
Schritt 4: Formular-Controller implementieren
Bevor wir Turbo eingeführt haben, haben wir einen einfachen Ereignislistener hinzugefügt, um das Formular nach dem Senden der Daten an den Server zurückzusetzen. Jetzt müssen wir diese Funktionalität wiederherstellen, aber ohne den alten Code erneut zu verwenden. Wir könnten ein Turbo-Stream verwenden, um das Formular zurückzusetzen, oder sogar ein Turbo-Frame, aber anstatt das zu tun, habe ich mich entschieden, eine andere Bibliothek des Hotwire-Frameworks zu verwenden, nämlich Stimulus:
Hier ist ein einfaches Code-Snippet für den Stimulus-Controller des Formulars:
import { Application, Controller } from "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
class FormController extends Controller {
clear() {
this.element.reset()
}
}
const application = Application.start();
application.register("form", FormController);
So sieht das HTML-Formular aus, mit Datenattributen, die den Controller mit dem DOM verknüpfen und die Ereignisse mit den entsprechenden Controller-Methoden verbinden:
<form id="chat-form" action="/submit" method="post" data-controller="form" data-action="turbo:submit-end->form#clear">
<label for="message-input">Message:</label>
<input name="message" data-form-target="input" required >
<input type="hidden" name="clientId" value="${clientId}">
<input type="submit" value="Send">
</form>
Das Ereignis, das in diesem Fall am besten für das Absenden des Formulars geeignet ist, ist turbo:submit-end. Gemäss der Dokumentation zu Stimulus-Deskriptoren können wir nach dem Ereignis des Formularabsendens die Methode #clear()
aufrufen. Wir verwenden nicht das submit
-Ereignis, da dies das Formular vorzeitig löschen würde.
Fazit
Hotwire ist ein JavaScript-Framework, das uns dabei hilft, Anwendungen interaktiver zu gestalten, während der JavaScript-Code minimal bleibt. Obwohl das Framework von den Autoren von Ruby on Rails erstellt wurde, ist es backend-agnostisch.
Turbo-Streams ermöglichen es uns, die Benutzeroberfläche des Clients asynchron zu aktualisieren, ohne dass dafür (in einigen Fällen nur sehr wenig) Frontend-Code erforderlich ist.
Mit Stimulus können wir einfaches JavaScript-Verhalten zu unserem HTML hinzufügen, indem wir Stimulus-Controller und Datenattribute verwenden.
Wo befindet sich der Code?
Die vollständige Chat-Anwendung mit zusätzlichen Funktionen wie:
Client-Identifikation über Query-Parameter
Zufällige Benutzernamen
Echtzeit-Benutzerliste
Das GitHub-Repository für dieses Projekt finden Sie hier: https://github.com/CuddlyBunion341/bunjs-turbo-demo