Ruby on Rails w WebAssembly – pełny pakiet w przeglądarce

Vladimir Dementyev
Vladimir Dementyev

Data publikacji: 31 stycznia 2025 r.

Wyobraź sobie, że w przeglądarce działa w pełni funkcjonalny blog – nie tylko interfejs, ale także backend. Bez serwerów i chmur – tylko Ty, Twoja przeglądarka i… WebAssembly. Umożliwiając uruchamianie frameworków po stronie serwera lokalnie, WebAssembly zaciera granice klasycznego rozwoju stron internetowych i otwiera nowe, ekscytujące możliwości. W tym poście Vladimir Dementyev (kierownik backendu w Evil Martians) informuje o postępach w dostosowywaniu Ruby on Rails do obsługi Wasm i przeglądarek:

  • Jak w 15 minut wprowadzić Railsa do przeglądarki
  • Za kulisami procesu tworzenia aplikacji w Rails.
  • Przyszłość Railsa i Wasm.

Znane z Ruby on Rails „blogowanie w 15 minut” teraz w Twojej przeglądarce

Ruby on Rails to framework internetowy, który zwiększa produktywność programistów i przyspiesza wdrażanie aplikacji. Jest to technologia używana przez liderów branży, takich jak GitHub i Shopify. Popularność tego frameworku zaczęła się wiele lat temu wraz z opublikowaniem przez Davida Heinemeiera Hanssona (czyli DHH) słynnego filmu „Jak stworzyć bloga w 15 minut”. W 2005 r. niemożliwe było stworzenie w tak krótkim czasie w pełni działającej aplikacji internetowej. To było jak magia.

Dzisiaj chcę przywrócić to magiczne uczucie, tworząc aplikację Rails, która działa w przeglądarce. Twoja podróż zaczyna się od utworzenia podstawowej aplikacji Rails w zwykły sposób, a potem skompilowania jej w formacie Wasm.

Wprowadzenie: „blog w 15 minut” w wierszu poleceń

Zakładając, że masz zainstalowane na komputerze Ruby i Ruby on Rails, zacznij od utworzenia nowej aplikacji Ruby on Rails i stworzenia szablonu niektórych funkcji (tak jak w oryginalnym filmie „Blog w 15 minut”):


$ 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) ===========

Bez dotykania kodu źródłowego możesz teraz uruchomić aplikację i zobaczyć, jak działa:

$ bin/dev

=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000

Teraz możesz otworzyć swój blog pod adresem http://localhost:3000/posts i zacząć pisać posty.

Blog Ruby on Rails uruchomiony z wiersza poleceń w przeglądarce.

Masz bardzo podstawową, ale funkcjonalną aplikację bloga, którą utworzyłeś/utworzyłaś w kilka minut. Jest to aplikacja full-stack sterowana przez serwer: masz bazę danych (SQLite) do przechowywania danych, serwer internetowy do obsługi żądań HTTP (Puma) oraz program Ruby do obsługi logiki biznesowej, udostępniania interfejsu użytkownika i przetwarzania interakcji z użytkownikiem. Na koniec jest cienka warstwa kodu JavaScript (Turbo), która ma na celu usprawnienie przeglądania.

Oficjalny pokaz aplikacji Rails kontynuuje wdrażanie tej aplikacji na serwer typu bare metal, aby była gotowa do wdrożenia w wersji produkcyjnej. Twoja podróż będzie przebiegać w przeciwnym kierunku: zamiast umieszczać aplikację gdzieś daleko, „wdrożysz” ją lokalnie.

Następny poziom: „blog w 15 minut” w Wasm

Od czasu dodania WebAssembly przeglądarki mogą uruchamiać nie tylko kod JavaScriptu, ale dowolny kod, który można skompilować do formatu Wasm. I Ruby nie jest wyjątkiem. Rails to nie tylko Ruby, ale zanim zaczniemy omawiać różnice, kontynuujmy prezentację i wasmify (czasownik ukuty przez bibliotekę wasmify-rails) aplikacji Rails.

Wystarczy, że wykonasz kilka poleceń, aby skompilować aplikację bloga w moduł Wasm i uruchomić ją w przeglądarce.

Najpierw zainstaluj bibliotekę wasmify-rails za pomocą Bundler (npm w Ruby) i uruchom jej generator za pomocą interfejsu wiersza poleceń 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!

Polecenie wasmify:rails konfiguruje specjalne środowisko wykonania „wasm” (oprócz domyślnych środowisk „development”, „test” i „production”) i instaluje wymagane zależności. W przypadku nowej aplikacji opartej na Rails wystarczy to, aby była ona gotowa do użycia z Wasm.

Następnie skompiluj podstawowy moduł Wasm zawierający środowisko wykonawcze Ruby, standardową bibliotekę i wszystkie zależności aplikacji:

$ 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

Ten krok może zająć trochę czasu: musisz skompilować Ruby z źródła, aby prawidłowo połączyć rozszerzenia natywne (napisane w języku C) z bibliotek innych firm. Ten (tymczasowy) problem omówimy w dalszej części tego posta.

Skompilowany moduł Wasm to tylko podstawa aplikacji. Musisz też spakować sam kod aplikacji oraz wszystkie zasoby (np. obrazy, CSS, JavaScript). Przed zapakowaniem utwórz podstawową aplikację uruchamiającą, która może służyć do uruchamiania wasmified Rails w przeglądarce. W tym celu możesz też użyć polecenia generatora:

$ bin/rails wasmify:pwa

  create  pwa
  create  pwa/boot.html
  create  pwa/boot.js
  ...
  prepend  config/wasmify.yml

Poprzednie polecenie generuje minimalną aplikację PWA utworzoną za pomocą Vite. Można jej używać lokalnie do testowania skompilowanego modułu Rails Wasm lub wdrożyć ją statycznie w celu dystrybucji.

Teraz, gdy masz już program uruchamiający, wystarczy spakować całą aplikację do jednego pliku binarnego Wasm:

$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB

Znakomicie. Uruchom aplikację uruchamiającą i sprawdź, czy aplikacja do blogowania w Rails działa w przeglądarce:

$ cd pwa/

$ yarn dev

  VITE v4.5.5  ready in 290 ms

    Local:   http://localhost:5173/

Otwórz adres http://localhost:5173, odczekaj chwilę, aż przycisk „Uruchom” stanie się aktywny, a potem kliknij go – możesz już korzystać z aplikacji Rails działającej lokalnie w przeglądarce.

Blog Ruby on Rails uruchomiony z karty przeglądarki w innej karcie przeglądarki.

Czy nie jest to jak magia, gdy monolityczna aplikacja po stronie serwera działa nie tylko na Twoim komputerze, ale też w piaskownicy przeglądarki? Dla mnie (chociaż jestem „czarownikiem”) to nadal wygląda na fantastykę. Nie ma w tym żadnej magii, tylko postęp technologiczny.

Prezentacja

Możesz obejrzeć wersję demonstracyjną umieszczoną w artykule lub uruchomić demo w osobnym oknie. Sprawdź kod źródłowy na GitHubie.

Kulisy projektu Rails on Wasm

Aby lepiej zrozumieć wyzwania (i rozwiązania) związane z pakowaniem aplikacji po stronie serwera do modułu Wasm, w pozostałych częściach tego artykułu wyjaśniono komponenty wchodzące w skład tej architektury.

Aplikacja internetowa zależy od wielu czynników, a nie tylko od języka programowania, w którym napisano kod aplikacji. Każdy komponent musi też zostać przeniesiony do _lokalnego środowiska wdrożenia_, czyli przeglądarki. Demo „Blog w 15 minut” pokazuje, że można to zrobić bez potrzeby przepisywania kodu aplikacji. Ten sam kod służył do uruchamiania aplikacji w klasycznym trybie po stronie serwera i w przeglądarce.

Komponenty, z których składa się aplikacja Ruby on Rails: serwer WWW, baza danych, kolejka i magazyn danych. Do tego dochodzą podstawowe komponenty Ruby: gemy, natywne rozszerzenia, narzędzia systemowe i Ruby VM.

Framework, np. Ruby on Rails, udostępnia interfejs, czyli abstrakcję, która umożliwia komunikację z elementami infrastruktury. W tej sekcji omawiamy, jak można wykorzystać architekturę frameworku do obsługi nieco ezoterycznych potrzeb wyświetlania lokalnego.

Podstawa: ruby.wasm

W 2022 roku (od wersji 3.2.0) Ruby oficjalnie obsługuje Wasm, co oznacza, że kod źródłowy C można skompilować do Wasm i przenosić maszynę wirtualną Ruby wszędzie tam, gdzie chcesz. Projekt ruby.wasm zawiera zkompilowane moduły i wiązania JavaScriptu do uruchamiania Ruby w przeglądarce (lub dowolnego innego środowiska JavaScript). Projekt ruby:wasm zawiera też narzędzia do kompilacji, które umożliwiają kompilowanie niestandardowej wersji Ruby z dodatkowymi zależnościami. Jest to bardzo ważne w przypadku projektów korzystających z bibliotek z rozszerzeniami C. Tak, możesz skompilować rozszerzenia natywne do formatu Wasm. (nie wszystkie, ale większość).

Obecnie Ruby w pełni obsługuje interfejs systemowy WebAssembly WASI 0.1. WASI 0.2, które obejmuje model komponentów, jest już w wersji alfa i jest o kilka kroków od ukończenia.Gdy zostanie dodane wsparcie dla WASI 0.2, nie będzie już trzeba ponownie kompilować całego języka za każdym razem, gdy trzeba dodać nowe natywne zależności: można je zmienić w komponenty.

Model komponentów powinien też pośrednio pomóc w zmniejszeniu rozmiaru pakietu. Więcej informacji o rozwoju i postępach ruby.wasm znajdziesz w wykładzie Co możesz robić z Ruby na WebAssembly.

Problem z Ruby w przypadku Wasm został rozwiązany. Ale Rails jako framework internetowy potrzebuje wszystkich komponentów pokazanych na poprzednim diagramie. Czytaj dalej, aby dowiedzieć się, jak umieszczać inne komponenty w przeglądarce i połączać je w Rails.

Łączenie z bazą danych działającą w przeglądarce

SQLite3 jest dostarczana z oficjalną dystrybucją Wasm i odpowiednim opakowaniem JavaScript, dzięki czemu jest gotowa do umieszczenia w przeglądarce. PostgreSQL for Wasm jest dostępny w ramach projektu PGlite. Musisz tylko dowiedzieć się, jak połączyć się z bazą danych w przeglądarce z aplikacji Rails on Wasm.

Komponent lub podramka Railsa odpowiedzialny za modelowanie danych i interakcje z bazą danych nazywa się Active Record (tak, nazwa pochodzi od wzorca projektowego ORM). Za pomocą adapterów baz danych Active Record odsuwa z kodu aplikacji implementację bazy danych obsługującej język SQL. Rails udostępnia gotowe adaptery SQLite3, PostgreSQL i MySQL. Wszystkie z nich zakładają jednak połączenie z prawdziwymi bazami danych dostępnymi w sieci. Aby to obejść, możesz napisać własne adaptery do łączenia się z lokalnymi bazami danych w przeglądarce.

Oto jak implementowane są adaptery SQLite3 Wasm i PGlite w ramach projektu Wasmify Rails:

  • Klasa adaptera dziedziczy z odpowiedniego wbudowanego adaptera (np. class PGliteAdapter < PostgreSQLAdapter), dzięki czemu możesz ponownie użyć logiki przygotowywania zapytań i analizowania wyników.
  • Zamiast połączenia z bazą danych na niskim poziomie używasz obiektu zewnętrznego interfejsu, który działa w czasie wykonywania JavaScriptu. Jest to most między modułem Rails Wasm a bazą danych.

Oto na przykład implementacja mostu dla 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();
    },
  };
}

Z perspektywy aplikacji przejście z rzeczywistej bazy danych na bazę w przeglądarce to tylko kwestia konfiguracji:

# config/database.yml
development:
  adapter: sqlite3

production:
  adapter: sqlite3

wasm:
  adapter: sqlite3_wasm
  js_interface: "sqliteForRails"

Praca z lokalną bazą danych nie wymaga dużego wysiłku. Jeśli jednak wymagana jest synchronizacja danych z jakiś centralnym źródłem informacji, możesz napotkać problemy na wyższym poziomie. To pytanie wykracza poza zakres tego posta (wskazówka: sprawdź demo Rails na PGlite i ElectricSQL).

Usługa w tle jako serwer WWW

Kolejnym niezbędnym elementem każdej aplikacji internetowej jest serwer WWW. Użytkownicy mogą wchodzić w interakcję z aplikacjami internetowymi za pomocą żądań HTTP. Dlatego musisz przekierowywać żądania HTTP wywoływane przez nawigację lub przesyłanie formularzy do modułu Wasm. Na szczęście przeglądarka ma na to rozwiązanie: service worker.

Usługa to specjalny rodzaj Web Workera, który działa jako serwer pośredniczący między aplikacją JavaScript a siecią. Może przechwytywać żądania i zmieniać je, np. wyświetlać dane z bufora, przekierowywać na inne adresy URL lub… do modułów Wasm. Oto szkic usługi obsługującej żądania za pomocą aplikacji Rails uruchomionej w 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)
  );
});

„Pobieranie” jest wywoływane za każdym razem, gdy przeglądarka wysyła żądanie. Możesz uzyskać informacje o żądaniu (URL, nagłówki HTTP, treść) i utworzyć własny obiekt żądania.

Rails, podobnie jak większość aplikacji internetowych w Ruby, korzysta z interfejsu Rack do obsługi żądań HTTP. Interfejs Rack opisuje format obiektów żądania i odpowiedzi, a także interfejs obsługi HTTP (aplikacji). Te właściwości możesz wyrazić w ten sposób:

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, {...}, [...]]

Jeśli format żądania wydaje Ci się znajomy, prawdopodobnie pracowałeś/pracowałaś wcześniej z CGI.

Obiekt JavaScript RackHandler odpowiada za konwertowanie żądań i odpowiedzi między domeną JavaScript a domeną Ruby. Ponieważ Rack jest używany przez większość aplikacji internetowych Ruby, implementacja staje się uniwersalna, a nie specyficzna dla Rails. Praktyczne wdrożenie jest jednak zbyt długie, aby zamieścić je tutaj.

Usługa w tle to jeden z kluczowych elementów aplikacji internetowej w przeglądarce. Jest to nie tylko serwer proxy HTTP, ale też warstwa buforowania i przełącznik sieci (czyli możesz tworzyć aplikacje w pierwszej kolejności lokalnej lub działające w trybie offline). Jest to też komponent, który może Ci pomóc w wyświetlaniu plików przesłanych przez użytkowników.

Przesyłanie plików w przeglądarce

Jedną z pierwszych dodatkowych funkcji, które zostaną zaimplementowane w nowej aplikacji bloga, będzie prawdopodobnie obsługa przesyłania plików, a ściślej mówiąc dołączania obrazów do postów. W tym celu musisz znaleźć sposób na przechowywanie i przesyłanie plików.

W Rails część frameworku odpowiedzialna za przesyłanie plików nosi nazwę Active Storage. Active Storage udostępnia programistom abstrakcje i interfejsy do pracy z plikami bez konieczności rozmyślania o mechanizmie przechowywania na niskim poziomie. Niezależnie od tego, gdzie przechowujesz pliki (na dysku twardym czy w chmurze), kod aplikacji nie ma o tym pojęcia.

Podobnie jak w przypadku Active Record, aby obsługiwać niestandardowy mechanizm pamięci, wystarczy zaimplementować odpowiedni adapter usługi pamięci. Gdzie przechowywać pliki w przeglądarce?

Tradycyjną opcją jest użycie bazy danych. Tak, możesz przechowywać pliki jako obiekty blob w bazie danych bez konieczności korzystania z dodatkowych komponentów infrastruktury. W Rails jest już gotowa wtyczka do tego celu – baza danych Active Storage. Jednak wyświetlanie plików przechowywanych w bazie danych za pomocą aplikacji Rails działającej w ramach WebAssembly nie jest idealnym rozwiązaniem, ponieważ wiąże się z cyklami (de)serializacji, które nie są bezpłatne.

Lepszym rozwiązaniem, bardziej zoptymalizowanym pod kątem przeglądarki, jest użycie interfejsów API systemu plików i przetwarzanie przesyłanych plików oraz plików przesłanych przez serwer bezpośrednio z procesu obsługi usługi. Idealnym kandydatem na taką infrastrukturę jest OPFS (origin private file system), czyli najnowszy interfejs API przeglądarki, który z pewnością odegra ważną rolę w przyszłych aplikacjach w przeglądarce.

Co można osiągnąć dzięki połączeniu Railsa i Wasm

Założę się, że podczas czytania tego artykułu zadawaliście sobie pytanie: dlaczego framework po stronie serwera ma działać w przeglądarce? Pojęcie frameworku lub biblioteki po stronie serwera (lub po stronie klienta) to tylko etykieta. Dobry kod i zwłaszcza dobra abstrakcja sprawdzają się wszędzie. Etykiety nie powinny powstrzymywać Cię przed odkrywaniem nowych możliwości i przekraczaniem granic frameworku (np. Ruby on Rails) ani granic środowiska wykonawczego (WebAssembly). Oba te rozwiązania mogą przynieść korzyści w takich niekonwencjonalnych przypadkach użycia.

Istnieje też wiele tradycyjnych lub praktycznych zastosowań.

Po pierwsze, przeniesienie frameworku do przeglądarki otwiera ogromne możliwości nauki i tworzenia prototypów. Wyobraź sobie, że możesz bawić się bibliotekami, wtyczkami i wzorami bezpośrednio w przeglądarce i razem z innymi osobami. Stackblitz umożliwił to w przypadku frameworków JavaScript. Innym przykładem jest WordPress Playground, który umożliwia eksperymentowanie z motywami WordPress bez potrzeby opuszczania strony internetowej. Wasm może umożliwić coś podobnego w przypadku Ruby i jej ekosystemu.

Istnieje specjalny przypadek kodowania w przeglądarce, który jest szczególnie przydatny dla programistów open source – rozpatrywanie i debugowanie problemów. Ponownie, StackBlitz ułatwia to w przypadku projektów JavaScript: tworzysz minimalny skrypt odtwarzania, wskazujesz link w problemie na GitHubie i oszczędzasz czas konserwatorom na odtwarzanie scenariusza. W Ruby jest to już możliwe dzięki projektowi RunRuby.dev (poniżej przykład problemu, który został rozwiązany dzięki odtworzeniu w przeglądarce).

Innym przypadkiem użycia są aplikacje obsługujące tryb offline (lub działające w trybie offline). Aplikacje działające w trybie offline, które zwykle korzystają z sieci, ale można z nich korzystać również bez połączenia. Może to być na przykład klient poczty e-mail, który umożliwia wyszukiwanie w skrzynce odbiorczej w trybie offline. Możesz też użyć aplikacji z muzyką, która umożliwia przechowywanie muzyki na urządzeniu, dzięki czemu ulubione utwory będą odtwarzane nawet wtedy, gdy nie będziesz mieć połączenia z internetem. Oba przykłady zależą od danych przechowywanych lokalnie, a nie tylko od pamięci podręcznej, jak w przypadku klasycznych PWAs.

Warto też budować aplikacje lokalne (czyli na komputery) za pomocą Rails, ponieważ framework ten zwiększa produktywność, a nie zależy od środowiska wykonawczego. Pełne ramy są odpowiednie do tworzenia aplikacji, które wykorzystują dużo danych osobowych i zawierają dużo logiki. Możliwe jest też użycie Wasm jako przenośnego formatu dystrybucji.

To dopiero początek przygody z Rails na Wasm. Więcej informacji o problemach i ich rozwiązaniach znajdziesz w ebooku Ruby on Rails na WebAssembly (który jest zresztą samą aplikacją Rails działającą offline).