發布日期:2025 年 1 月 31 日
想像一下,您可以在瀏覽器中執行功能完整的網誌,不只前端,後端也能正常運作。不涉及伺服器或雲端,只有您、您的瀏覽器和… WebAssembly!透過允許伺服器端架構在本機執行,WebAssembly 模糊了傳統網頁開發的界線,並開啟令人振奮的新可能性。在這篇文章中,Vladimir Dementyev (Evil Martians 的後端主管) 將分享讓 Ruby on Rails 支援 Wasm 和瀏覽器的進度:
- 如何在 15 分鐘內將 Rails 整合至瀏覽器。
- Rails 的 wasmification 幕後。
- Rails 和 Wasm 的未來。
Ruby on Rails 的著名「15 分鐘建立網誌」教學課程現在可在瀏覽器中執行
Ruby on Rails 是專注於開發人員工作效率和快速出貨的網路架構。這項技術是業界領導者 (例如 GitHub 和 Shopify) 所使用的技術。這個架構的熱門程度可追溯至多年前,當時 David Heinemeier Hansson (或稱 DHH) 發布了著名的「How to build a blog in 15 minutes」影片,在 2005 年,要想在如此短的時間內建構出完整運作的網頁應用程式,是不可想像的。這就像是魔法!
今天,我想透過建立在瀏覽器中完整執行的 Rails 應用程式,重現這種神奇的感覺。您可以先按照慣用方式建立基本 Rails 應用程式,然後為 Wasm 進行封裝。
背景資訊:在指令列中使用「15 分鐘建立網誌」
假設您已在電腦上安裝 Ruby 和 Ruby on Rails,請先建立新的 Ruby on Rails 應用程式,並搭建一些功能 (就像原始的「15 分鐘建立部落格」影片一樣):
$ 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) ===========
您現在可以執行應用程式,並查看應用程式運作情形,而無須修改程式碼:
$ bin/dev
=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000
您現在可以前往 http://localhost:3000/posts 開啟自己的網誌,並開始撰寫文章!
您只需幾分鐘,就能建構出功能完整的簡易版網誌應用程式。這是一個全端、由伺服器控制的應用程式:您有資料庫 (SQLite) 可儲存資料、網路伺服器可處理 HTTP 要求 (Puma),以及 Ruby 程式可保留業務邏輯、提供 UI 和處理使用者互動。最後,我們還提供一層輕量版 JavaScript (Turbo),讓瀏覽體驗更流暢。
官方 Rails 示範會繼續將這個應用程式部署至裸機伺服器,讓應用程式可正式上線。您將會繼續朝相反方向前進:您不會將應用程式放在遠端,而是會在本機「部署」應用程式。
進階課程:使用 Wasm 製作「15 分鐘的部落格」
自從加入 WebAssembly 後,瀏覽器不僅能執行 JavaScript 程式碼,還能執行任何可編譯成 Wasm 的程式碼。Ruby 也不例外。當然,Rails 不只包含 Ruby,但在深入探討差異之前,讓我們繼續進行示範,並將 Rails 應用程式轉換為 WASM (由 wasmify-rails 程式庫創造的動詞)!
您只需執行幾個指令,即可將網誌應用程式編譯為 Wasm 模組,並在瀏覽器中執行。
首先,您必須使用 Bundler (Ruby 的 npm
) 安裝 wasmify-rails 程式庫,然後使用 Rails CLI 執行產生器:
$ 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!
wasmify:rails
指令會設定專屬的「wasm」執行環境 (除了預設的「開發」、「測試」和「正式」環境),並安裝必要的依附元件。對於全新的 Rails 應用程式而言,這就足以讓應用程式支援 Wasm。
接下來,請建構核心 Wasm 模組,其中包含 Ruby 執行階段、標準程式庫和所有應用程式依附元件:
$ 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
這個步驟可能需要一些時間:您必須從原始碼建構 Ruby,才能正確連結第三方程式庫的原生擴充功能 (以 C 語言編寫)。本文稍後會說明這個 (暫時) 缺點。
編譯的 Wasm 模組只是應用程式的基礎。您也必須封裝應用程式程式碼本身和所有資產 (例如圖片、CSS、JavaScript)。在進行封裝之前,請建立可用於在瀏覽器中執行 wasmified Rails 的基本啟動器應用程式。為此,也有產生器指令:
$ bin/rails wasmify:pwa
create pwa
create pwa/boot.html
create pwa/boot.js
...
prepend config/wasmify.yml
前述指令會產生使用 Vite 建構的最低限 PWA 應用程式,可在本機測試編譯的 Rails Wasm 模組,或以靜態方式部署以發布應用程式。
有了啟動工具,您只需將整個應用程式打包至單一 Wasm 二進位檔即可:
$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB
大功告成!執行啟動器應用程式,並在瀏覽器中查看 Rails 部落格應用程式完整執行情形:
$ cd pwa/
$ yarn dev
VITE v4.5.5 ready in 290 ms
➜ Local: http://localhost:5173/
前往 http://localhost:5173,稍待「Launch」按鈕啟用,然後點選該按鈕,即可在瀏覽器中本機執行 Rails 應用程式!
這不就是神奇之處嗎?您不但可以在電腦上,還可以在瀏覽器沙箱中執行單體伺服器端應用程式。對我來說 (即使我是「巫師」),這仍像是天方夜譚。但這並非魔法,只是技術的進步。
示範
您可以體驗文章中嵌入的示範,或是在獨立視窗中啟動示範。請參閱 GitHub 上的原始碼。
Wasm 上的 Rails 幕後故事
為了進一步瞭解將伺服器端應用程式封裝至 Wasm 模組的挑戰 (和解決方案),本篇文章的其餘部分將說明這個架構的組件。
網路應用程式依賴的項目不只是用來編寫應用程式程式碼的程式設計語言,每個元件也必須導入_本機部署環境_,也就是瀏覽器。令人振奮的是,「15 分鐘完成部落格」示範不需要重新編寫應用程式程式碼,在經典的伺服器端模式和瀏覽器中,使用相同的程式碼執行應用程式。
像是 Ruby on Rails 這樣的架構會提供介面,也就是用來與基礎架構元件通訊的抽象概念。以下章節將說明如何運用架構架構,滿足某些特殊的本機服務需求。
基礎:ruby.wasm
Ruby 已於 2022 年正式支援 Wasm (自 3.2.0 版起),這表示 C 原始碼可編譯為 Wasm,並在您想要的任何位置提供 Ruby VM。ruby.wasm 專案會提供預先編譯的模組和 JavaScript 繫結,以便在瀏覽器 (或任何其他 JavaScript 執行階段) 中執行 Ruby。ruby:wasm 專案也提供建構工具,可讓您建構自訂的 Ruby 版本,並納入其他依附元件。對於依賴含 C 擴充功能的程式庫的專案而言,這點非常重要。沒錯,您也可以將原生擴充功能編譯成 Wasm!(雖然目前還沒有任何擴充功能,但大部分的擴充功能都已支援)。
Ruby 目前完全支援 WebAssembly 系統介面 WASI 0.1。WASI 0.2 包含元件模型,目前處於 Alpha 版狀態,距離完成還有幾個步驟。一旦支援 WASI 0.2,您就無需每次新增原生依附元件時重新編譯整個語言,因為這些元件可進行元件化。
元件模型也會有助於縮減套件大小。如要進一步瞭解 ruby.wasm 的開發和進度,請參閱「在 WebAssembly 上使用 Ruby 可做哪些事」演講。
因此,Wasm 方程式的 Ruby 部分已解決。不過,Rails 是網頁架構,因此需要上圖所示的所有元件。請繼續閱讀,瞭解如何將其他元件放入瀏覽器,並在 Rails 中將這些元件連結在一起。
連線至在瀏覽器中執行的資料庫
SQLite3 隨附官方 Wasm 發行版本和相應的 JavaScript 包裝函式,因此可在瀏覽器中嵌入。您可以透過 PGlite 專案取得 Wasm 適用的 PostgreSQL。因此,您只需要瞭解如何從 Wasm 上的 Rails 應用程式連線至瀏覽器內資料庫。
Rails 的元件或子架構負責資料建模和資料庫互動,稱為 Active Record (是的,是以 ORM 設計模式命名)。Active Record 會透過資料庫轉接程式,將實際的 SQL 資料庫實作從應用程式程式碼中抽離。Rails 會提供 SQLite3、PostgreSQL 和 MySQL 轉接程式。不過,這些方法都假設會連線至透過網路提供的實際資料庫。為解決這個問題,您可以自行編寫轉接程式,連線至本機的瀏覽器資料庫!
以下是如何建立 SQLite3 Wasm 和 PGlite 轉接程式,並在 Wasmify Rails 專案中實作:
- 轉接器類別會繼承對應的內建轉接器 (例如
class PGliteAdapter < PostgreSQLAdapter
),因此您可以重複使用實際的查詢準備和結果剖析邏輯。 - 您可以使用位於 JavaScript 執行階段的外部介面物件,而非低階資料庫連線,這類似於 Rails Wasm 模組與資料庫之間的橋樑。
舉例來說,以下是 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();
},
};
}
從應用程式角度來看,從實際資料庫轉換至瀏覽器內資料庫,只需進行設定即可:
# config/database.yml
development:
adapter: sqlite3
production:
adapter: sqlite3
wasm:
adapter: sqlite3_wasm
js_interface: "sqliteForRails"
使用本機資料庫不需要花費太多心力。不過,如果需要將資料與某些中央可靠來源進行同步處理,您可能會面臨更高層級的挑戰。這個問題超出本篇文章的範圍 (提示:請查看 PGlite 和 ElectricSQL 上的 Rails 示範)。
以服務工作者做為網路伺服器
任何網頁應用程式都需要網路伺服器這個重要元件。使用者會透過 HTTP 要求與網頁應用程式互動。因此,您需要一種方法,將導覽或表單提交作業觸發的 HTTP 要求,轉送至 Wasm 模組。幸好,瀏覽器有服務工作者來解決這個問題。
服務工作站是一種特殊的 Web Worker,可做為 JavaScript 應用程式與網路之間的 Proxy。它可以攔截及操控要求,例如:提供快取資料、重新導向至其他網址或… Wasm 模組!以下是使用在 Wasm 中執行的 Rails 應用程式,為服務提供要求的服務工作草圖:
// 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)
);
});
每次瀏覽器發出要求時,系統就會觸發「擷取」作業。您可以取得要求資訊 (網址、HTTP 標頭、主體),並建構自己的要求物件。
和大多數 Ruby 網路應用程式一樣,Rails 會使用 Rack 介面處理 HTTP 要求。Rack 介面會說明要求和回應物件的格式,以及底層 HTTP 處理常式 (應用程式) 的介面。您可以使用下列方式表示這些屬性:
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, {...}, [...]]
如果您發現要求格式很熟悉,那麼您可能曾在過去使用過 CGI。
RackHandler
JavaScript 物件負責在 JavaScript 和 Ruby 領域之間轉換要求和回應。由於大多數 Ruby 網頁應用程式都使用 Rack,因此實作方式就會變成通用,而非 Rails 專屬。不過,實際實作的內容太長,無法在這裡貼出。
服務工作者是瀏覽器內網頁應用程式的重要整合點之一。它不僅是 HTTP Proxy,也是快取層和網路切換器 (也就是說,您可以建構優先使用本機或可離線使用的應用程式)。這個元件也可以協助您提供使用者上傳的檔案。
在瀏覽器中保留檔案上傳作業
在全新的網誌應用程式中,您可能會首先實作支援檔案上傳功能,更具體來說,就是在文章中附加圖片。如要做到這點,您需要能夠儲存及提供檔案的方式。
在 Rails 中,負責處理檔案上傳作業的架構部分稱為「Active Storage」。Active Storage 可為開發人員提供抽象和介面,讓他們不必考慮低階儲存機制,即可處理檔案。無論您將檔案儲存在硬碟或雲端,應用程式程式碼都不會察覺。
與 Active Record 類似,如要支援自訂儲存機制,您只需實作對應的儲存服務轉接器即可。在瀏覽器中儲存檔案的位置?
傳統做法是使用資料庫。可以,您可以在資料庫中以 blob 格式儲存檔案,不需要額外的基礎架構元件。在 Rails 中,已經有專為此目的外掛程式 Active Storage Database。不過,透過在 WebAssembly 中執行的 Rails 應用程式,提供儲存在資料庫中的檔案並不理想,因為這會涉及一連串的 (反)序列化作業,而這些作業並非免費。
更佳且更適合瀏覽器的解決方案是使用檔案系統 API,並直接從服務 worker 處理檔案上傳作業和伺服器上傳的檔案。OPFS (原始私人檔案系統) 是這類基礎架構的絕佳候選項目,這是最新的瀏覽器 API,在未來的瀏覽器內應用程式中將扮演重要角色。
Rails 和 Wasm 搭配使用可達成的目標
我很確定,在您開始閱讀本文時,您一定會問自己這個問題:為什麼要在瀏覽器中執行伺服器端架構?將架構或程式庫視為伺服器端 (或用戶端) 的想法只是一個標籤。優質程式碼和優質抽象化功能,無論在何處都能發揮作用。標籤不應阻止您探索新可能性,以及突破架構 (例如 Ruby on Rails) 和執行階段 (WebAssembly) 的界限。這兩種技術都可能從這類非傳統用途中受益。
還有許多傳統或實用的用途。
首先,將架構帶入瀏覽器,就能帶來大量的學習和原型設計機會。想像一下,您可以直接在瀏覽器中與其他人一起使用程式庫、外掛程式和模式。Stackblitz 可讓您在 JavaScript 架構中執行這項操作。另一個例子是 WordPress Playground,可讓您不必離開網頁,就能試用 WordPress 主題。Wasm 可為 Ruby 及其生態系統啟用類似的功能。
瀏覽器內程式設計有一個特殊用途,特別適合開放原始碼開發人員使用,那就是分類和偵錯問題。同樣地,StackBlitz 也為 JavaScript 專案提供這項功能:您可以建立最小化的重現指令碼,指向 GitHub 問題中的連結,讓維護人員不必花時間重現您的情境。事實上,這項工作已經在 Ruby 中開始進行,這要歸功於 RunRuby.dev 專案 (以下是透過瀏覽器內重現解決的問題範例)。
另一個用途是離線功能 (或離線感知) 應用程式。可離線使用的應用程式通常會使用網路運作,但在沒有連線時仍可使用。例如電子郵件用戶端,可讓您在離線時搜尋收件匣。或者,音樂庫應用程式具備「儲存在裝置上」功能,即使沒有網路連線,也能持續播放喜愛的音樂。這兩個範例都依賴儲存在本機的資料,而非像傳統 PWA 一樣使用快取。
最後,使用 Rails 建構本機 (或電腦版) 應用程式也是合理的做法,因為該架構提供的生產力不依賴執行階段。功能齊全的架構非常適合用於建構大量使用個人資料和邏輯的應用程式。使用 Wasm 做為可移植的發布格式也是可行的做法。
這只是 Rails on Wasm 旅程的開端。如要進一步瞭解挑戰和解決方案,請參閱 Ruby on Rails on WebAssembly 電子書 (順帶一提,這本書本身就是可離線使用的 Rails 應用程式)。