Publié le 31 janvier 2025
Imaginez exécuter un blog entièrement fonctionnel dans votre navigateur, non seulement en termes de frontend, mais aussi de backend. Aucun serveur ni cloud n'est impliqué : vous, votre navigateur et… WebAssembly ! En permettant aux frameworks côté serveur de s'exécuter localement, WebAssembly efface les limites du développement Web classique et ouvre de nouvelles possibilités passionnantes. Dans cet article, Vladimir Dementyev (responsable du backend chez Evil Martians) partage les progrès réalisés pour rendre Ruby on Rails compatible avec Wasm et les navigateurs:
- Intégrer Rails dans le navigateur en 15 minutes
- Coulisses de la wasmification de Rails
- L'avenir de Rails et de Wasm
Le célèbre "blog en 15 minutes" de Ruby on Rails est désormais disponible directement dans votre navigateur
Ruby on Rails est un framework Web axé sur la productivité des développeurs et la rapidité de déploiement. C'est la technologie utilisée par des leaders du secteur tels que GitHub et Shopify. La popularité du framework a commencé il y a de nombreuses années avec la publication de la célèbre vidéo How to build a blog in 15 minutes (Comment créer un blog en 15 minutes) de David Heinemeier Hansson (ou DHH). En 2005, il était impensable de créer une application Web entièrement fonctionnelle en si peu de temps. C'était comme de la magie !
Aujourd'hui, je voudrais recréer cette sensation magique en créant une application Rails qui s'exécute entièrement dans votre navigateur. Votre parcours commence par la création d'une application Rails de base de la manière habituelle, puis par son empaquetage pour Wasm.
Contexte: un "blog en 15 minutes" sur la ligne de commande
En supposant que Ruby et Ruby on Rails soient installés sur votre ordinateur, commencez par créer une application Ruby on Rails et échafauder certaines fonctionnalités (comme dans la vidéo d'origine "Blogging in 15 minutes"):
$ 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) ===========
Sans même toucher au code de base, vous pouvez maintenant exécuter l'application et la voir en action:
$ bin/dev
=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000
Vous pouvez maintenant ouvrir votre blog à l'adresse http://localhost:3000/posts et commencer à écrire des articles.
Vous disposez d'une application de blog très basique, mais fonctionnelle, créée en quelques minutes. Il s'agit d'une application full stack contrôlée par le serveur: vous disposez d'une base de données (SQLite) pour stocker vos données, d'un serveur Web pour gérer les requêtes HTTP (Puma) et d'un programme Ruby pour conserver votre logique métier, fournir une UI et traiter les interactions utilisateur. Enfin, une fine couche de code JavaScript (Turbo) permet de simplifier l'expérience de navigation.
La démonstration officielle de Rails poursuit le déploiement de cette application sur un serveur bare metal, ce qui la rend prête à la production. Votre parcours se poursuivra dans la direction opposée: au lieu de placer votre application quelque part à distance, vous la "déployez" localement.
Niveau supérieur: un "blog en 15 minutes" en Wasm
Depuis l'ajout de WebAssembly, les navigateurs peuvent exécuter non seulement du code JavaScript, mais aussi tout code compilable en Wasm. Et Ruby ne fait pas exception. Rails est bien plus que Ruby, mais avant d'examiner les différences, poursuivons la démonstration et wasmifions (verbe inventé par la bibliothèque wasmify-rails) l'application Rails.
Il vous suffit d'exécuter quelques commandes pour compiler votre application de blog dans un module Wasm et l'exécuter dans le navigateur.
Commencez par installer la bibliothèque wasmify-rails à l'aide de Bundler (le npm
de Ruby) et exécutez son générateur à l'aide de la CLI Rails:
$ 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!
La commande wasmify:rails
configure un environnement d'exécution "wasm" dédié (en plus des environnements "développement", "test" et "production" par défaut) et installe les dépendances requises. Pour une application Rails vierge, cela suffit à la rendre compatible avec Wasm.
Ensuite, créez le module Wasm principal contenant l'environnement d'exécution Ruby, la bibliothèque standard et toutes les dépendances de l'application:
$ 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
Cette étape peut prendre un certain temps: vous devez compiler Ruby à partir de la source pour associer correctement les extensions natives (écrites en C) des bibliothèques tierces. Cet inconvénient (temporaire) sera abordé plus loin dans cet article.
Le module Wasm compilé n'est qu'une base pour votre application. Vous devez également empaqueter le code de l'application lui-même et tous les composants (par exemple, des images, des fichiers CSS et JavaScript). Avant de procéder à l'empaquetage, créez une application de lanceur de base qui peut être utilisée pour exécuter Rails wasmifié dans le navigateur. Pour cela, il existe également une commande de générateur:
$ bin/rails wasmify:pwa
create pwa
create pwa/boot.html
create pwa/boot.js
...
prepend config/wasmify.yml
La commande précédente génère une application PWA minimale créée avec Vite, qui peut être utilisée localement pour tester le module Wasm Rails compilé ou être déployée de manière statique pour distribuer l'application.
Avec le lanceur, il vous suffit d'empaqueter l'ensemble de l'application dans un seul fichier binaire Wasm:
$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB
Et voilà ! Exécutez l'application de lancement et regardez votre application de bloggage Rails s'exécuter entièrement dans le navigateur:
$ cd pwa/
$ yarn dev
VITE v4.5.5 ready in 290 ms
➜ Local: http://localhost:5173/
Accédez à http://localhost:5173, attendez un peu que le bouton "Lancer" devienne actif, puis cliquez dessus. Profitez de l'application Rails exécutée en local dans votre navigateur.
N'est-ce pas magique d'exécuter une application monolithique côté serveur non seulement sur votre machine, mais aussi dans le bac à sable du navigateur ? Pour moi (même si je suis le "sorcier"), cela ressemble toujours à de la fantaisie. Mais il n'y a pas de magie, seulement le progrès de la technologie.
Démonstration
Vous pouvez tester la démonstration intégrée à l'article ou la lancer dans une fenêtre autonome. Consultez le code source sur GitHub.
Coulisses de Rails sur Wasm
Pour mieux comprendre les défis (et les solutions) liés à l'empaquetage d'une application côté serveur dans un module Wasm, le reste de cet article explique les composants qui font partie de cette architecture.
Une application Web dépend de bien d'autres éléments qu'un simple langage de programmation utilisé pour écrire le code de l'application. Chaque composant doit également être importé dans votre _environnement de déploiement local_, le navigateur. L'aspect intéressant de la démonstration "Blogue en 15 minutes" est qu'elle peut être réalisée sans avoir à réécrire le code de l'application. Le même code a été utilisé pour exécuter l'application en mode classique côté serveur et dans le navigateur.
Un framework, comme Ruby on Rails, vous fournit une interface, une abstraction pour communiquer avec les composants d'infrastructure. La section suivante explique comment utiliser l'architecture du framework pour répondre aux besoins de diffusion locale quelque peu ésotériques.
La base: ruby.wasm
Ruby est officiellement compatible avec Wasm depuis 2022 (depuis la version 3.2.0), ce qui signifie que le code source C peut être compilé en Wasm et qu'une VM Ruby peut être installée partout où vous le souhaitez. Le projet ruby.wasm fournit des modules précompilés et des liaisons JavaScript pour exécuter Ruby dans le navigateur (ou tout autre environnement d'exécution JavaScript). Le projet ruby:wasm est également fourni avec les outils de compilation qui vous permettent de créer une version Ruby personnalisée avec des dépendances supplémentaires. Cela est très important pour les projets qui s'appuient sur des bibliothèques avec des extensions C. Oui, vous pouvez également compiler des extensions natives en Wasm. (encore aucune extension, mais la plupart d'entre elles).
Actuellement, Ruby est entièrement compatible avec l'interface système WebAssembly, WASI 0.1. WASI 0.2, qui inclut le modèle de composants, est déjà en phase alpha et à quelques étapes de la finalisation.Une fois WASI 0.2 pris en charge, il n'est plus nécessaire de recompiler l'ensemble du langage chaque fois que vous devez ajouter de nouvelles dépendances natives: elles peuvent être modélisées en composants.
En outre, le modèle de composants devrait également contribuer à réduire la taille du bundle. Pour en savoir plus sur le développement et la progression de ruby.wasm, consultez la conférence What you can do with Ruby on WebAssembly (Ce que vous pouvez faire avec Ruby sur WebAssembly).
La partie Ruby de l'équation Wasm est donc résolue. Cependant, en tant que framework Web, Rails a besoin de tous les composants présentés dans le schéma précédent. Lisez la suite pour découvrir comment insérer d'autres composants dans le navigateur et les associer dans Rails.
Se connecter à une base de données exécutée dans le navigateur
SQLite3 est fourni avec une distribution Wasm officielle et un wrapper JavaScript correspondant. Il est donc prêt à être intégré dans le navigateur. PostgreSQL pour Wasm est disponible via le projet PGlite. Par conséquent, vous n'avez qu'à découvrir comment vous connecter à la base de données dans le navigateur à partir de l'application Rails on Wasm.
Un composant, ou sous-framework, de Rails chargé de la modélisation des données et des interactions avec la base de données est appelé Active Record (oui, du nom du modèle de conception ORM). Active Record éloigne l'implémentation de la base de données SQL du code de l'application via les adaptateurs de base de données. Rails fournit des adaptateurs SQLite3, PostgreSQL et MySQL prêts à l'emploi. Toutefois, ils supposent tous une connexion à des bases de données réelles disponibles sur le réseau. Pour y remédier, vous pouvez écrire vos propres adaptateurs pour vous connecter à des bases de données locales dans le navigateur.
Voici comment les adaptateurs SQLite3 Wasm et PGlite implémentés dans le projet Wasmify Rails sont créés:
- La classe d'adaptateur hérite de l'adaptateur intégré correspondant (par exemple,
class PGliteAdapter < PostgreSQLAdapter
), ce qui vous permet de réutiliser la logique de préparation et d'analyse des résultats de la requête. - Au lieu de la connexion de base de données de bas niveau, vous utilisez un objet interface externe qui se trouve dans l'environnement d'exécution JavaScript, un pont entre un module Rails Wasm et une base de données.
Par exemple, voici l'implémentation du pont pour 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();
},
};
}
Du point de vue de l'application, le passage d'une base de données réelle à une base de données dans le navigateur n'est qu'une question de configuration:
# config/database.yml
development:
adapter: sqlite3
production:
adapter: sqlite3
wasm:
adapter: sqlite3_wasm
js_interface: "sqliteForRails"
Travailler avec une base de données locale ne nécessite pas beaucoup d'efforts. Toutefois, si la synchronisation des données avec une source de vérité centrale est requise, vous devrez peut-être faire face à un défi de niveau supérieur. Cette question n'entre pas dans le champ d'application de cet article (conseil: consultez la démo Rails sur PGlite et ElectricSQL).
Service worker en tant que serveur Web
Un autre composant essentiel de toute application Web est un serveur Web. Les utilisateurs interagissent avec les applications Web à l'aide de requêtes HTTP. Vous avez donc besoin d'un moyen de router les requêtes HTTP déclenchées par la navigation ou l'envoi de formulaires vers votre module Wasm. Heureusement, le navigateur a la solution : les service workers.
Un service worker est un type particulier de Web Worker qui sert de proxy entre l'application JavaScript et le réseau. Il peut intercepter les requêtes et les manipuler, par exemple: diffuser des données mises en cache, rediriger vers d'autres URL ou… vers des modules Wasm ! Voici un croquis d'un service qui traite les requêtes à l'aide d'une application Rails exécutée dans Wasm:
// 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)
);
});
La récupération est déclenchée chaque fois qu'une requête est envoyée par le navigateur. Vous pouvez obtenir les informations de la requête (URL, en-têtes HTTP, corps) et créer votre propre objet de requête.
Comme la plupart des applications Web Ruby, Rails s'appuie sur l'interface Rack pour gérer les requêtes HTTP. L'interface Rack décrit le format des objets de requête et de réponse, ainsi que l'interface du gestionnaire HTTP sous-jacent (application). Vous pouvez exprimer ces propriétés comme suit:
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, {...}, [...]]
Si le format de la requête vous a semblé familier, vous avez probablement déjà travaillé avec CGI.
L'objet JavaScript RackHandler
est chargé de convertir les requêtes et les réponses entre les domaines JavaScript et Ruby. Étant donné que Rack est utilisé par la plupart des applications Web Ruby, l'implémentation devient universelle, et non spécifique à Rails.
L'implémentation réelle est cependant trop longue pour être publiée ici.
Un service worker est l'un des éléments clés d'une application Web dans le navigateur. Il s'agit non seulement d'un proxy HTTP, mais également d'une couche de mise en cache et d'un commutateur réseau (c'est-à-dire que vous pouvez créer une application axée sur le local ou compatible avec le mode hors connexion). Il s'agit également d'un composant qui peut vous aider à diffuser des fichiers importés par les utilisateurs.
Conserver les importations de fichiers dans le navigateur
L'une des premières fonctionnalités supplémentaires à implémenter dans votre nouvelle application de blog est probablement la prise en charge de l'importation de fichiers, ou plus précisément de l'ajout d'images aux posts. Pour ce faire, vous devez disposer d'un moyen de stocker et de diffuser des fichiers.
Dans Rails, la partie du framework chargée de gérer les importations de fichiers s'appelle Active Storage. Active Storage fournit aux développeurs des abstractions et des interfaces pour travailler avec des fichiers sans se soucier du mécanisme de stockage de bas niveau. Où que vous stockiez vos fichiers, sur un disque dur ou dans le cloud, le code de l'application n'en a pas connaissance.
Comme pour Active Record, pour prendre en charge un mécanisme de stockage personnalisé, il vous suffit d'implémenter un adaptateur de service de stockage correspondant. Où stocker les fichiers dans le navigateur ?
L'option traditionnelle consiste à utiliser une base de données. Oui, vous pouvez stocker des fichiers en tant que blobs dans la base de données, sans aucun composant d'infrastructure supplémentaire. Il existe déjà un plug-in prêt à l'emploi dans Rails, la base de données Active Storage. Toutefois, la diffusion de fichiers stockés dans une base de données via l'application Rails exécutée dans WebAssembly n'est pas idéale, car elle implique des cycles de (dé)sérialisation qui ne sont pas sans frais.
Une solution plus efficace et plus optimisée pour les navigateurs consiste à utiliser des API de système de fichiers et à traiter les importations de fichiers et les fichiers importés par le serveur directement à partir du worker de service. Le OPFS (origin private file system), une API de navigateur très récente qui jouera certainement un rôle important pour les futures applications dans le navigateur, est un candidat idéal pour une telle infrastructure.
Ce que Rails et Wasm peuvent accomplir ensemble
Je suis sûr que vous vous êtes posé cette question lorsque vous avez commencé à lire cet article: pourquoi exécuter un framework côté serveur dans le navigateur ? L'idée d'un framework ou d'une bibliothèque côté serveur (ou côté client) n'est qu'un étiquetage. Un bon code et, surtout, une bonne abstraction fonctionnent partout. Les étiquettes ne doivent pas vous empêcher d'explorer de nouvelles possibilités et de repousser les limites du framework (par exemple, Ruby on Rails) ainsi que les limites de l'environnement d'exécution (WebAssembly). Ces deux cas d'utilisation non conventionnels pourraient en bénéficier.
Il existe également de nombreux cas d'utilisation conventionnels ou pratiques.
Tout d'abord, l'intégration du framework au navigateur ouvre d'énormes opportunités d'apprentissage et de prototypage. Imaginez pouvoir jouer avec des bibliothèques, des plug-ins et des modèles directement dans votre navigateur et avec d'autres personnes. Stackblitz a rendu cela possible pour les frameworks JavaScript. Un autre exemple est un espace de jeu WordPress qui permettait de jouer avec des thèmes WordPress sans quitter la page Web. Wasm pourrait permettre quelque chose de similaire pour Ruby et son écosystème.
Il existe un cas particulier de codage dans le navigateur particulièrement utile aux développeurs Open Source : le triage et le débogage des problèmes. Encore une fois, StackBlitz a rendu cela possible pour les projets JavaScript: vous créez un script de reproduction minimal, pointez vers le lien dans un problème GitHub et épargnez aux mainteneurs le temps de reproduire votre scénario. Et, en fait, cela a déjà commencé en Ruby grâce au projet RunRuby.dev (voici un exemple de problème résolu avec la reproduction dans le navigateur).
Autre cas d'utilisation : les applications compatibles avec le mode hors connexion (ou compatibles avec le mode hors connexion). Applications compatibles avec le mode hors connexion qui fonctionnent généralement sur le réseau, mais qui restent utilisables en cas de non-connexion. Par exemple, un client de messagerie qui vous permet de rechercher dans votre boîte de réception lorsque vous êtes hors connexion. Vous pouvez également utiliser une application de bibliothèque musicale avec la fonctionnalité "Stocker sur l'appareil" pour que votre musique préférée continue de jouer même en l'absence de connexion réseau. Les deux exemples dépendent des données stockées localement, et non pas uniquement d'un cache comme avec les PWA classiques.
Enfin, la création d'applications locales (ou pour ordinateur) avec Rails est également judicieuse, car la productivité que le framework vous offre ne dépend pas de l'environnement d'exécution. Les frameworks complets sont adaptés à la création d'applications axées sur les données à caractère personnel et la logique. L'utilisation de Wasm comme format de distribution portable est également une option viable.
Ce n'est que le début de ce parcours Rails on Wasm. Pour en savoir plus sur les défis et les solutions, consultez l'e-book Ruby on Rails sur WebAssembly (qui est, par ailleurs, une application Rails compatible avec le mode hors connexion).