Veröffentlicht: 31. Januar 2025
Stellen Sie sich vor, Sie würden einen voll funktionsfähigen Blog in Ihrem Browser betreiben – nicht nur das Frontend, sondern auch das Backend. Es sind keine Server oder Clouds erforderlich – nur Sie, Ihr Browser und WebAssembly. Da serverseitige Frameworks lokal ausgeführt werden können, verwischt WebAssembly die Grenzen der klassischen Webentwicklung und eröffnet spannende neue Möglichkeiten. In diesem Beitrag berichtet Vladimir Dementyev (Leiter des Backends bei Evil Martians) über die Fortschritte bei der Wasm- und Browser-Unterstützung von Ruby on Rails:
- Rails in 15 Minuten in den Browser bringen
- Hinter den Kulissen der Wasmifizierung von Rails
- Die Zukunft von Rails und Wasm
Der berühmte Ruby on Rails-Artikel „Blog in 15 Minuten“ jetzt direkt in Ihrem Browser
Ruby on Rails ist ein Web-Framework, das auf die Produktivität von Entwicklern und die schnelle Bereitstellung ausgerichtet ist. Diese Technologie wird von Branchenführern wie GitHub und Shopify verwendet. Die Beliebtheit des Frameworks begann vor vielen Jahren mit der Veröffentlichung des berühmten Videos „How to build a blog in 15 minutes“ (Wie man in 15 Minuten einen Blog erstellt) von David Heinemeier Hansson (DHH). 2005 war es unvorstellbar, in so kurzer Zeit eine voll funktionsfähige Webanwendung zu erstellen. Es war magisch!
Heute möchte ich dieses magische Gefühl wieder aufleben lassen, indem ich eine Rails-Anwendung erstelle, die vollständig in Ihrem Browser ausgeführt wird. Sie beginnen damit, auf die übliche Weise eine einfache Rails-Anwendung zu erstellen und sie dann für Wasm zu verpacken.
Hintergrund: „Blog in 15 Minuten“ in der Befehlszeile
Angenommen, Sie haben Ruby und Ruby on Rails auf Ihrem Computer installiert, erstellen Sie zuerst eine neue Ruby on Rails-Anwendung und erstellen Sie ein Gerüst für einige Funktionen (genau wie im ursprünglichen Video „Blog in 15 Minuten“):
$ rails new --css=tailwind web_dev_blog
create .ruby-version
...
$ cd web_dev_blog
$ bin/rails generate scaffold Post title:string date:date body:text
create db/migrate/20241217183624_create_posts.rb
create app/models/post.rb
...
$ bin/rails db:migrate
== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
-> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========
Sie können die Anwendung jetzt ausführen und in Aktion sehen, ohne die Codebasis zu ändern:
$ bin/dev
=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000
Jetzt können Sie Ihren Blog unter http://localhost:3000/posts öffnen und Beiträge verfassen.
Sie haben in wenigen Minuten eine sehr einfache, aber funktionsfähige Blog-Anwendung erstellt. Es ist eine Full-Stack-, servergesteuerte Anwendung: Sie haben eine Datenbank (SQLite) zum Speichern Ihrer Daten, einen Webserver zum Verarbeiten von HTTP-Anfragen (Puma) und ein Ruby-Programm, um Ihre Geschäftslogik zu verwalten, eine Benutzeroberfläche bereitzustellen und Nutzerinteraktionen zu verarbeiten. Schließlich gibt es noch eine dünne JavaScript-Schicht (Turbo), die das Surfen optimiert.
In der offiziellen Rails-Demo wird diese Anwendung auf einem Bare-Metal-Server bereitgestellt und somit für die Produktion vorbereitet. Sie gehen nun in die entgegengesetzte Richtung: Anstatt Ihre Anwendung irgendwo weit weg zu platzieren, stellen Sie sie lokal bereit.
Nächste Stufe: Ein „Blog in 15 Minuten“ in Wasm
Seit der Einführung von WebAssembly können Browser nicht nur JavaScript-Code, sondern auch jeden Code ausführen, der in Wasm kompiliert werden kann. Ruby ist da keine Ausnahme. Sicherlich ist Rails mehr als nur Ruby, aber bevor wir uns mit den Unterschieden befassen, fahren wir mit der Demo fort und wasmify (ein Verb, das von der Bibliothek wasmify-rails geprägt wurde) die Rails-Anwendung.
Sie müssen nur einige Befehle ausführen, um Ihre Bloganwendung in ein Wasm-Modul zu kompilieren und im Browser auszuführen.
Installieren Sie zuerst die wasmify-rails-Bibliothek mit Bundler (der npm
von Ruby) und führen Sie den Generator mit der Rails-Befehlszeile aus:
$ bundle add wasmify-rails
$ bin/rails wasmify:install
create config/wasmify.yml
create config/environments/wasm.rb
...
info ✅ The application is prepared for Wasm-ificaiton!
Mit dem Befehl wasmify:rails
wird eine spezielle Ausführungsumgebung für „wasm“ konfiguriert (zusätzlich zu den Standardumgebungen „development“, „test“ und „production“) und die erforderlichen Abhängigkeiten werden installiert. Bei einer neuen Rails-Anwendung reicht das aus, um sie Wasm-kompatibel zu machen.
Erstellen Sie als Nächstes das Wasm-Kernmodul mit der Ruby-Laufzeit, der Standardbibliothek und allen Anwendungsabhängigkeiten:
$ bin/rails wasmify:build
==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB
Dieser Schritt kann einige Zeit in Anspruch nehmen: Sie müssen Ruby aus der Quelle kompilieren, um native Erweiterungen (in C geschrieben) aus den Drittanbieterbibliotheken richtig zu verknüpfen. Auf diesen (vorübergehenden) Nachteil wird später in diesem Beitrag eingegangen.
Das kompilierte Wasm-Modul ist nur eine Grundlage für Ihre Anwendung. Außerdem müssen Sie den Anwendungscode selbst und alle Assets (z. B. Bilder, CSS, JavaScript) verpacken. Erstellen Sie vor dem Packen eine einfache Launcher-Anwendung, mit der die Wasm-kompilierte Rails-Anwendung im Browser ausgeführt werden kann. Dazu gibt es auch einen Generatorbefehl:
$ bin/rails wasmify:pwa
create pwa
create pwa/boot.html
create pwa/boot.js
...
prepend config/wasmify.yml
Mit dem vorherigen Befehl wird eine minimale PWA-Anwendung generiert, die mit Vite erstellt wurde. Sie kann lokal verwendet werden, um das kompilierte Rails-Wasm-Modul zu testen, oder statisch bereitgestellt werden, um die App zu verteilen.
Mit dem Launcher müssen Sie jetzt nur noch die gesamte Anwendung in einem einzigen Wasm-Binärprogramm verpacken:
$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB
Geschafft! Führen Sie die Launcher-App aus und sehen Sie sich Ihre Rails-Blogging-Anwendung vollständig im Browser an:
$ cd pwa/
$ yarn dev
VITE v4.5.5 ready in 290 ms
➜ Local: http://localhost:5173/
Rufen Sie http://localhost:5173 auf, warten Sie, bis die Schaltfläche „Starten“ aktiv wird, und klicken Sie darauf. Viel Spaß mit der Rails-Anwendung, die lokal in Ihrem Browser ausgeführt wird.
Ist es nicht magisch, eine monolithische serverseitige Anwendung nicht nur auf Ihrem Computer, sondern auch in der Browser-Sandbox auszuführen? Für mich (auch wenn ich der „Zauberer“ bin) sieht es immer noch wie eine Fantasie aus. Aber es gibt keine Zauberei, nur den Fortschritt der Technologie.
Demo
Sie können die Demo im Artikel eingebettet ansehen oder in einem eigenständigen Fenster starten. Quellcode auf GitHub ansehen
Hinter den Kulissen von Rails on Wasm
Um die Herausforderungen (und Lösungen) beim Einpacken einer serverseitigen Anwendung in ein Wasm-Modul besser zu verstehen, werden im Rest dieses Artikels die Komponenten dieser Architektur erläutert.
Eine Webanwendung hängt von vielen mehr Dingen ab als nur von einer Programmiersprache, mit der der Anwendungscode geschrieben wird. Jede Komponente muss auch in Ihre lokale Bereitstellungsumgebung gebracht werden – den Browser. Das Besondere an der Demo „Blog in 15 Minuten“ ist, dass dies ohne Neuschreiben des Anwendungscodes möglich ist. Derselbe Code wurde verwendet, um die Anwendung im klassischen, serverseitigen Modus und im Browser auszuführen.
Ein Framework wie Ruby on Rails bietet eine Schnittstelle, eine Abstraktion, um mit Infrastrukturkomponenten zu kommunizieren. Im folgenden Abschnitt wird erläutert, wie Sie die Framework-Architektur für die etwas esoterische lokale Auslieferung nutzen können.
Die Grundlage: ruby.wasm
Ruby ist seit 2022 (Version 3.2.0) offiziell Wasm-kompatibel. Das bedeutet, dass der C-Quellcode in Wasm kompiliert werden kann und eine Ruby-VM überall eingesetzt werden kann. Das Projekt ruby.wasm enthält vorab kompilierte Module und JavaScript-Bindungen, um Ruby im Browser (oder in einer anderen JavaScript-Laufzeit) auszuführen. Das ruby:wasm-Projekt enthält auch die Build-Tools, mit denen Sie eine benutzerdefinierte Ruby-Version mit zusätzlichen Abhängigkeiten erstellen können. Das ist sehr wichtig für Projekte, die auf Bibliotheken mit C-Erweiterungen angewiesen sind. Ja, Sie können auch native Erweiterungen in Wasm kompilieren. (Noch nicht für alle Erweiterungen, aber für die meisten).
Derzeit unterstützt Ruby die WebAssembly System Interface, WASI 0.1, vollständig. WASI 0.2, das das Komponentenmodell enthält, befindet sich bereits im Alpha-Status und ist nur noch wenige Schritte von der Fertigstellung entfernt. Sobald WASI 0.2 unterstützt wird, müssen Sie die gesamte Sprache nicht mehr jedes Mal neu kompilieren, wenn Sie neue native Abhängigkeiten hinzufügen möchten. Sie können dann komponentenbasiert sein.
Als Nebeneffekt sollte das Komponentenmodell auch dazu beitragen, die Größe des Bundles zu reduzieren. Weitere Informationen zur Entwicklung und den Fortschritten von ruby.wasm finden Sie im Vortrag Was Sie mit Ruby on WebAssembly tun können.
Damit ist der Ruby-Teil der Wasm-Gleichung gelöst. Als Web-Framework benötigt Rails jedoch alle im vorherigen Diagramm dargestellten Komponenten. Im Folgenden erfahren Sie, wie Sie weitere Komponenten in den Browser einfügen und in Rails verknüpfen.
Verbindung zu einer Datenbank herstellen, die im Browser ausgeführt wird
SQLite3 wird mit einer offiziellen Wasm-Distribution und einem entsprechenden JavaScript-Wrapper geliefert und kann daher im Browser eingebettet werden. PostgreSQL for Wasm ist über das Projekt PGlite verfügbar. Sie müssen also nur herausfinden, wie Sie über die Rails on Wasm-Anwendung eine Verbindung zur In-Browser-Datenbank herstellen.
Eine Komponente oder ein Unter-Framework von Rails, das für die Datenmodellierung und Datenbankinteraktionen verantwortlich ist, heißt Active Record (ja, nach dem ORM-Designmuster). Active Record abstrahiert die eigentliche SQL-Datenbankimplementierung über die Datenbankadapter vom Anwendungscode. Rails bietet standardmäßig SQLite3-, PostgreSQL- und MySQL-Adapter. Sie gehen jedoch alle davon aus, dass eine Verbindung zu echten Datenbanken hergestellt wird, die über das Netzwerk verfügbar sind. Um dieses Problem zu lösen, können Sie eigene Adapter schreiben, um eine Verbindung zu lokalen In-Browser-Datenbanken herzustellen.
So werden SQLite3 Wasm- und PGlite-Adapter erstellt, die im Rahmen des Wasmify Rails-Projekts implementiert wurden:
- Die Adapterklasse wird vom entsprechenden integrierten Adapter (z. B.
class PGliteAdapter < PostgreSQLAdapter
) abgeleitet, sodass Sie die Logik für die Abfragevorbereitung und das Parsen von Ergebnissen wiederverwenden können. - Anstelle der Low-Level-Datenbankverbindung verwenden Sie ein externes Interface-Objekt, das in der JavaScript-Laufzeit vorhanden ist – eine Brücke zwischen einem Rails-Wasm-Modul und einer Datenbank.
Hier ist beispielsweise die Bridge-Implementierung für SQLite3 Wasm:
export function registerSQLiteWasmInterface(worker, db, opts = {}) {
const name = opts.name || "sqliteForRails";
worker[name] = {
exec: function (sql) {
let cols = [];
let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });
return {
cols,
rows,
};
},
changes: function () {
return db.changes();
},
};
}
Aus Sicht der Anwendung ist die Umstellung von einer echten Datenbank auf eine In-Browser-Datenbank nur eine Frage der Konfiguration:
# config/database.yml
development:
adapter: sqlite3
production:
adapter: sqlite3
wasm:
adapter: sqlite3_wasm
js_interface: "sqliteForRails"
Die Arbeit mit einer lokalen Datenbank ist nicht sehr aufwendig. Wenn jedoch die Datensynchronisierung mit einer zentralen Source of Truth erforderlich ist, stehen Sie möglicherweise vor einer größeren Herausforderung. Diese Frage fällt nicht in den Rahmen dieses Artikels. Sehen Sie sich die Demo „Rails on PGlite and ElectricSQL“ an.
Dienst-Worker als Webserver
Eine weitere wichtige Komponente jeder Webanwendung ist ein Webserver. Nutzer interagieren mit Webanwendungen über HTTP-Anfragen. Daher benötigen Sie eine Möglichkeit, HTTP-Anfragen, die durch Navigation oder Formulareinreichungen ausgelöst werden, an Ihr Wasm-Modul weiterzuleiten. Glücklicherweise hat der Browser eine Lösung dafür: Dienstworker.
Ein Dienst-Worker ist eine spezielle Art von Web-Worker, der als Proxy zwischen der JavaScript-Anwendung und dem Netzwerk dient. Es kann Anfragen abfangen und manipulieren, z. B. gecachte Daten bereitstellen, zu anderen URLs weiterleiten oder… zu Wasm-Modulen! Hier ist eine Skizze eines Dienstes, der Anfragen mit einer Rails-Anwendung verarbeitet, die in Wasm ausgeführt wird:
// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;
const initVM = async (progress, opts = {}) => {
if (vm) return vm;
if (!db) {
await initDB(progress);
}
vm = await initRailsVM("/app.wasm");
return vm;
};
const rackHandler = new RackHandler(initVM});
self.addEventListener("fetch", (event) => {
// ...
return event.respondWith(
rackHandler.handle(event.request)
);
});
„fetch“ wird jedes Mal ausgelöst, wenn der Browser eine Anfrage sendet. Sie können die Anfrageinformationen (URL, HTTP-Header, Text) abrufen und Ihr eigenes Anfrageobjekt erstellen.
Wie die meisten Ruby-Webanwendungen verwendet Rails die Rack-Schnittstelle für die Verarbeitung von HTTP-Anfragen. Die Rack-Schnittstelle beschreibt das Format der Anfrage- und Antwortobjekte sowie die Schnittstelle des zugrunde liegenden HTTP-Handlers (Anwendung). Sie können diese Properties so ausdrücken:
request = {
"REQUEST_METHOD" => "GET",
"SCRIPT_NAME" => "",
"SERVER_NAME" => "localhost",
"SERVER_PORT" => "3000",
"PATH_INFO" => "/posts"
}
handler = proc do |env|
[
200,
{"Content-Type" => "text/html"},
["<!doctype html><html><body>Hello Web!</body></html>"]
]
end
handler.call(request) #=> [200, {...}, [...]]
Wenn Ihnen das Anfrageformat bekannt vorkommt, haben Sie wahrscheinlich schon einmal mit CGI gearbeitet.
Das JavaScript-Objekt RackHandler
ist für die Umwandlung von Anfragen und Antworten zwischen JavaScript- und Ruby-Bereichen verantwortlich. Da Rack von den meisten Ruby-Webanwendungen verwendet wird, wird die Implementierung universell und nicht Rails-spezifisch.
Die tatsächliche Implementierung ist jedoch zu lang, um sie hier zu veröffentlichen.
Ein Dienst-Worker ist einer der wichtigsten Bestandteile einer In-Browser-Webanwendung. Es ist nicht nur ein HTTP-Proxy, sondern auch eine Caching-Ebene und ein Netzwerk-Schalter. Das bedeutet, dass Sie eine lokale oder offlinefähige Anwendung erstellen können. Diese Komponente kann auch beim Bereitstellen von von Nutzern hochgeladenen Dateien helfen.
Dateiuploads im Browser speichern
Eine der ersten zusätzlichen Funktionen, die Sie in Ihrer neuen Bloganwendung implementieren sollten, ist die Unterstützung von Dateiuploads, genauer gesagt das Anhängen von Bildern an Beiträge. Dazu benötigen Sie eine Möglichkeit zum Speichern und Bereitstellen von Dateien.
In Rails wird der Teil des Frameworks, der für die Verarbeitung von Dateiuploads verantwortlich ist, Active Storage genannt. Active Storage bietet Entwicklern Abstraktionsschichten und Schnittstellen, mit denen sie mit Dateien arbeiten können, ohne sich Gedanken über den Low-Level-Speichermechanismus machen zu müssen. Ganz gleich, wo Sie Ihre Dateien speichern, auf einer Festplatte oder in der Cloud, der Anwendungscode bleibt davon unberührt.
Ähnlich wie bei Active Record müssen Sie zum Unterstützen eines benutzerdefinierten Speichermechanismus nur einen entsprechenden Speicherdienstadapter implementieren. Wo werden Dateien im Browser gespeichert?
Die traditionelle Option ist die Verwendung einer Datenbank. Ja, Sie können Dateien als Blobs in der Datenbank speichern. Es sind keine zusätzlichen Infrastrukturkomponenten erforderlich. In Rails gibt es bereits ein fertiges Plug-in dafür: Active Storage Database. Das Bereitstellen von in einer Datenbank gespeicherten Dateien über die Rails-Anwendung, die in WebAssembly ausgeführt wird, ist jedoch nicht ideal, da dies mehrere (De-)Serialisierungsrunden erfordert, die nicht kostenlos sind.
Eine bessere und browseroptimiertere Lösung wäre die Verwendung von Dateisystem-APIs und die Verarbeitung von Dateiuploads und vom Server hochgeladenen Dateien direkt über den Dienst-Worker. Ein perfekter Kandidat für eine solche Infrastruktur ist das OPFS (Origin Private File System), eine neue Browser-API, die in Zukunft eine wichtige Rolle für In-Browser-Anwendungen spielen wird.
Was Rails und Wasm gemeinsam erreichen können
Ich bin mir ziemlich sicher, dass Sie sich diese Frage gestellt haben, als Sie mit dem Lesen des Artikels begonnen haben: Warum ein serverseitiges Framework im Browser ausführen? Die Bezeichnung „serverseitig“ oder „clientseitig“ ist nur ein Label. Guter Code und vor allem eine gute Abstraktion funktionieren überall. Labels sollten Sie nicht daran hindern, neue Möglichkeiten zu entdecken und die Grenzen des Frameworks (z. B. Ruby on Rails) sowie die Grenzen der Laufzeit (WebAssembly) zu erweitern. Beide könnten von solchen unkonventionellen Anwendungsfällen profitieren.
Es gibt auch viele konventionelle oder praktische Anwendungsfälle.
Erstens: Wenn Sie das Framework in den Browser einbinden, eröffnen sich enorme Lern- und Prototyping-Möglichkeiten. Stellen Sie sich vor, Sie könnten direkt in Ihrem Browser und gemeinsam mit anderen an Bibliotheken, Plug-ins und Mustern herumspielen. Stackblitz ermöglichte dies für JavaScript-Frameworks. Ein weiteres Beispiel ist ein WordPress-Playground, mit dem sich WordPress-Themes ausprobieren ließen, ohne die Webseite zu verlassen. Wasm könnte etwas Ähnliches für Ruby und sein Ökosystem ermöglichen.
Es gibt einen Sonderfall des In-Browser-Codings, der vor allem für Open-Source-Entwickler nützlich ist: Priorisierung und Behebung von Problemen. Auch hier hat StackBlitz eine Lösung für JavaScript-Projekte entwickelt: Sie erstellen ein minimales Reproduktionsskript, verweisen in einem GitHub-Problemfall auf den Link und sparen den Entwicklern die Zeit für die Reproduktion Ihres Szenarios. Und tatsächlich ist das in Ruby bereits dank des Projekts RunRuby.dev der Fall. Hier ist ein Beispielproblem, das mit der Reproduktion im Browser behoben wurde.
Ein weiterer Anwendungsfall sind offlinefähige (oder offline-orientierte) Anwendungen. Offlinefähige Anwendungen, die normalerweise über das Netzwerk funktionieren, aber auch ohne Verbindung verwendet werden können. Beispielsweise ein E-Mail-Client, mit dem Sie Ihren Posteingang auch offline durchsuchen können. Oder eine Musikbibliotheks-App mit der Funktion „Auf dem Gerät speichern“, damit Ihre Lieblingsmusik auch ohne Netzwerkverbindung weiter abgespielt wird. Bei beiden Beispielen werden lokal gespeicherte Daten verwendet, nicht nur ein Cache wie bei klassischen PWAs.
Außerdem ist es sinnvoll, lokale (oder Desktop-)Anwendungen mit Rails zu erstellen, da die Produktivität des Frameworks nicht von der Laufzeit abhängt. Vollständige Frameworks eignen sich gut für die Entwicklung von Anwendungen mit vielen personenbezogenen Daten und Logik. Auch die Verwendung von Wasm als portables Bereitstellungsformat ist eine gute Option.
Das ist erst der Anfang dieser Reise mit Rails on Wasm. Weitere Informationen zu den Herausforderungen und Lösungen finden Sie im eBook Ruby on Rails on WebAssembly, das übrigens selbst eine offlinefähige Rails-Anwendung ist.