发布时间:2025 年 1 月 31 日
想象一下,您可以在浏览器中运行一个功能齐全的博客,不仅包括前端,还包括后端。无需服务器或云端支持,只需您、您的浏览器和 WebAssembly 即可!通过允许在本地运行服务器端框架,WebAssembly 正在打破传统 Web 开发的边界,并开辟令人兴奋的新可能性。在本文中,Evil Martians 的后端主管 Vladimir Dementyev 分享了使 Ruby on Rails 支持 Wasm 和浏览器的进展:
- 如何在 15 分钟内将 Rails 引入浏览器。
- Rails wasmification 的幕后。
- Rails 和 Wasm 的未来。
Ruby on Rails 的著名“15 分钟创建博客”功能现已在您的浏览器中推出
Ruby on Rails 是一个 Web 框架,专注于提高开发者的工作效率并快速交付产品。GitHub 和 Shopify 等行业领军企业都采用了这种技术。该框架的普及始于多年前,当时 David Heinemeier Hansson(简称 DHH)发布了著名的“如何在 15 分钟内构建一个博客”视频。早在 2005 年,我们就无法想象能在如此短的时间内构建一个完全正常运行的 Web 应用。这感觉就像魔法一样!
今天,我想通过创建一个完全在浏览器中运行的 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) 来存储数据,一个 Web 服务器来处理 HTTP 请求 (Puma),以及一个 Ruby 程序来保留业务逻辑、提供界面并处理用户互动。最后,还有一层薄薄的 JavaScript (Turbo) 来简化浏览体验。
官方 Rails 演示将继续部署此应用到裸机服务器,以便其可以投入生产环境使用。您的旅程将继续朝相反方向前进:您将应用“部署”到本地,而不是放置在遥远的地方。
更进一步:在 Wasm 中“15 分钟写一篇博文”
自从添加 WebAssembly 以来,浏览器不仅能够运行 JavaScript 代码,还能够运行可编译为 Wasm 的任何代码。Ruby 也不例外。当然,Rails 不仅仅是 Ruby,但在深入探讨差异之前,让我们继续演示并将 Rails 应用转换为 WebAssembly 版本(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。
接下来,构建包含 Ruby 运行时、标准库和所有应用依赖项的核心 Wasm 模块:
$ 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)。在进行打包之前,请创建一个基本启动器应用,以便在浏览器中运行 wasm 化的 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 上的源代码。
Rails on Wasm 的幕后
为了更好地了解将服务器端应用打包到 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 后,您每次需要添加新的原生依赖项时都无需重新编译整个语言,因为它们可以组件化。
作为副作用,组件模型还应该有助于缩减软件包大小。您可以通过 What you can do with Ruby on WebAssembly 演讲详细了解 ruby.wasm 的开发和进展。
因此,Wasm 方程的 Ruby 部分已解。但作为 Web 框架,Rails 需要前面图表中显示的所有组件。请继续阅读,了解如何将其他组件放入浏览器并在 Rails 中将它们关联起来。
连接到在浏览器中运行的数据库
SQLite3 附带官方 Wasm 发行版和相应的 JavaScript 封装容器,因此可以嵌入到浏览器中。您可以通过 PGlite 项目获取适用于 Wasm 的 PostgreSQL。因此,您只需了解如何从 Rails on Wasm 应用连接到浏览器内数据库。
Rails 中负责数据建模和数据库交互的组件或子框架称为 Active Record(是的,它是以 ORM 设计模式命名的)。Active Record 通过数据库适配器将实际的 SQL 数据库实现从应用代码中抽象出来。Rails 开箱即用,可为您提供 SQLite3、PostgreSQL 和 MySQL 适配器。不过,它们都假定连接到网络上可用的真实数据库。为解决此问题,您可以编写自己的适配器来连接到本地浏览器内数据库!
以下是作为 Wasmify Rails 项目的一部分实现的 SQLite3 Wasm 和 PGlite 适配器的创建方式:
- 适配器类会继承相应的内置适配器(例如
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"
使用本地数据库不需要花费太多精力。但是,如果需要与某个中央可信来源同步数据,您可能会面临更高级别的挑战。此问题超出了本文的讨论范围(提示:请查看 Rails on PGlite and ElectricSQL 演示)。
将服务工作为 Web 服务器
任何 Web 应用的另一个重要组件是 Web 服务器。用户使用 HTTP 请求与 Web 应用互动。因此,您需要一种方法来将由导航或表单提交触发的 HTTP 请求路由到 Wasm 模块。幸运的是,浏览器有相应的解决方案:服务工件。
服务工件是一种特殊类型的 Web 工作器,可充当 JavaScript 应用与网络之间的代理。它可以拦截请求并对其进行操作,例如:提供缓存数据、重定向到其他网址或... 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 Web 应用一样,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 Web 应用都使用 Rack,因此实现变得通用,而非特定于 Rails。不过,实际实现太长,无法在此处发布。
Service Worker 是浏览器内 Web 应用的关键集成点之一。它不仅是 HTTP 代理,还是一种缓存层和网络切换器(也就是说,您可以构建本地优先或支持离线的应用)。此组件还可以帮助您分发用户上传的文件。
在浏览器中保留上传的文件
在全新的博客应用中要实现的首批附加功能之一可能是支持文件上传,或者更具体地说,支持将图片附加到帖子中。为此,您需要一种存储和传送文件的方法。
在 Rails 中,负责处理文件上传的框架部分称为 Active Storage。Active Storage 为开发者提供了抽象和接口,让他们无需考虑底层存储机制即可处理文件。无论您将文件存储在硬盘上还是在云端,应用代码都不会知晓。
与 Active Record 类似,如需支持自定义存储机制,您只需实现相应的存储服务适配器即可。在浏览器中将文件存储在哪里?
传统的做法是使用数据库。可以,您可以将文件作为 blob 存储在数据库中,无需任何额外的基础架构组件。Rails 中已经有现成的插件可用于此目的,即 Active Storage 数据库。不过,通过在 WebAssembly 中运行的 Rails 应用提供存储在数据库中的文件并不理想,因为这涉及一系列非免费的序列化和反序列化操作。
更好且更适合浏览器的解决方案是使用文件系统 API,并直接从服务工作器处理文件上传和服务器上传的文件。OPFS(源私有文件系统)是此类基础架构的理想人选,这是一个非常新的浏览器 API,对于未来的浏览器内应用肯定会发挥重要作用。
Rails 和 Wasm 可以共同实现什么
我敢肯定,在开始阅读本文时,您一定会问自己一个问题:为什么要在浏览器中运行服务器端框架?框架或库是服务器端(或客户端)的概念只是一个标签。优质的代码,尤其是优质的抽象,在任何地方都适用。标签不应阻止您探索新的可能性,也不应阻止您突破框架(例如 Ruby on Rails)和运行时(WebAssembly)的边界。这两种设备都可能受益于此类非传统用例。
还有许多传统或实用用例。
首先,将该框架引入浏览器可带来巨大的学习和原型设计机会。想象一下,您可以直接在浏览器中与他人一起玩转库、插件和模式。Stackblitz 让 JavaScript 框架也能做到这一点。另一个示例是 WordPress Playground,它让用户无需离开网页即可试用 WordPress 主题。Wasm 可以为 Ruby 及其生态系统实现类似的功能。
浏览器内编码有一个特殊用例,对开源开发者特别有用,那就是排查和调试问题。同样,StackBlitz 也为 JavaScript 项目提供了此功能:您只需创建一个最小的重现脚本,指向 GitHub 问题中的链接,即可让维护者省去重现您场景的时间。事实上,得益于 RunRuby.dev 项目,Ruby 中已经开始出现这种情况(下面是一个通过浏览器内重现解决的示例问题)。
另一个用例是支持离线(或感知离线)的应用。可离线使用的应用通常使用网络运行,但在没有连接的情况下,用户仍可使用这些应用。例如,电子邮件客户端可让您在离线状态下搜索收件箱。或者,具有“存储在设备上”功能的音乐库应用,这样即使没有网络连接,您喜爱的音乐也能继续播放。这两个示例都依赖于本地存储的数据,而不仅仅像传统 PWA 那样使用缓存。
最后,使用 Rails 构建本地(或桌面)应用也很有意义,因为该框架为您带来的高效性不依赖于运行时。功能齐全的框架非常适合构建包含大量个人数据和逻辑的应用。使用 Wasm 作为便携式分发格式也是一个可行的选择。
这只是 Rails on Wasm 之旅的开端。您可以参阅 Ruby on Rails on WebAssembly 电子书(顺便提一下,该电子书本身就是一个支持离线使用的 Rails 应用)详细了解相关挑战和解决方案。