はじめに
この記事は、Ruby/Rails Advent Calendar 2025 シリーズ 2、23日目の記事です。
こんにちは、@t0yohei です。
みなさん、ruby.wasm は使っていますか?
Ruby でフロントエンドを書けるなんて、なんて良い時代でしょうか。
そんな恵まれた時代にありつつも、あるようでなかった ruby.wasm 用のフロントエンドフレームワークを作ってみたので、是非とも紹介させてください。
Ruwi(RubyWasmUi) とは
Ruwi は ruwi.wasm 用のフロントエンドフレームワークです。
今風のフロントエンドフレームワークとして、以下の思想を反映させたフレームワークになっています。
- 宣言的 UI
- コンポーネント指向
- .......
今風と言うにはまだまだ色々足りないですね...。
これからに期待ということで...🙏
Ruwi ができること
じゃあ Ruwi で実際どんなことができるのか。
コードを見た方がわかりやすいと思うので、サンプルコードを見てみましょう。
<!DOCTYPE html>
<html>
<head>
<script defer src="https://unpkg.com/ruwi@0.10.0"></script>
<script defer type="text/ruby" src="app.rb"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
require "js"
# Hello World component
HelloComponent = Ruwi.define_component(
# props を利用して state を初期化
state: ->(props) {
{ message: props[:message] }
},
# component のレンダリング用メソッド
template: ->() {
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
<div>
<h2>{state[:message]}</h2>
<button on="{ click: -> { update_message } }">
Click me!
</button>
</div>
HTML
},
# component のメソッド
methods: {
update_message: ->() {
update_state(message: "You clicked the button!")
}
}
)
# Ruwi アプリケーションの作成とマウント
app = Ruwi::App.create(HelloComponent, message: "Hello, Ruwi!")
app_element = JS.global[:document].getElementById("app")
app.mount(app_element)
html ファイルを用意しておいて、コンポーネントを作成。そのコンポーネントを html ファイルの特定の要素にマウントする、といういつものあれです。
React や Vue.js などのフロントエンドフレームワークに馴染みがある方は、なんとなくコードの意味が理解できるかと思います。
というよりも大体それらをパクって作っているので、それはそうという感じです。
コンポーネントは、もちろん一度定義した後に何度でも使い回ることができます。
次の例では ButtonComponent を定義して CounterComponent で複数回使い回しています。
require "js"
CounterComponent = Ruwi.define_component(
state: ->(props) {
{ count: props[:count] || 0 }
},
template: ->() {
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
<div>
<div>{state[:count]}</div>
<ButtonComponent
label="Increment"
on="{ click_button: -> { increment } }">
</ButtonComponent>
<button-component
label="Decrement"
on="{ click_button: -> { decrement } }"
/>
</div>
HTML
},
methods: {
increment: ->() {
update_state(count: state[:count] + 1)
},
decrement: ->() {
update_state(count: state[:count] - 1)
}
}
)
ButtonComponent = Ruwi.define_component(
template: ->() {
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
<button on="{ click: ->() { emit('click_button') } }">
{props[:label]}
</button>
HTML
}
)
app = Ruwi::App.create(CounterComponent, count: 5)
app_element = JS.global[:document].getElementById("app")
app.mount(app_element)
当たり前のように思えることが当たり前にできて嬉しい!!
Ruwi を Gem として利用する
これまでの実装は Ruwi を CDN 経由で利用する場合の例です。
簡単なアプリケーションではそれで十分なのですが、もう少し複雑なアプリケーションを作りたくなった場合は、Ruwi を Gem として利用することもできます。
Ruwi を Gem として利用する場合は、実装した ruby ファイルを wasm ファイルに pack することになります。
React や Vue.js を JavaScript にビルドして使うみたいなのと一緒ですね。
セットアップ
-
ruwiをGemfileに追加する
# frozen_string_literal: true
source "https://rubygems.org"
gem "ruwi"
- gem をインストール
bundle install
Ruwi アプリケーションの作成
プロジェクトを初期化(初回のみ実施)
bundle exec ruwi setup
setup コマンドによりプロジェクトの構成は以下のようになります。
my-app/
├── Gemfile
└── src/
├── index.rb
└── index.html
index.rb, index.html にはボイラープレートとして以下のコードが実装されています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
<script type="module">
import { DefaultRubyVM } from "https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.7.2/dist/browser/+esm";
const response = await fetch("../src.wasm");
const module = await WebAssembly.compileStreaming(response);
const { vm } = await DefaultRubyVM(module);
vm.evalAsync(`
require "ruwi"
require_relative './src/index.rb'
`);
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
CounterComponent = Ruwi.define_component(
state: ->(props) {
{ count: props[:count] || 0 }
},
template: ->() {
Ruwi::Template::Parser.parse_and_eval(<<~HTML, binding)
<div>
<div>{state[:count]}</div>
<button on="{ click: -> { increment } }">Increment</button>
</div>
HTML
},
methods: {
increment: ->() {
update_state(count: state[:count] + 1)
}
}
)
app = Ruwi::App.create(CounterComponent, count: 0)
app_element = JS.global[:document].getElementById("app")
app.mount(app_element)
Ruwi アプリケーションの開発
ruwi dev コマンドで開発サーバーを起動して実装することができます。
開発サーバー起動中はファイルの保存を検知して、自動で wasm ファイルに pack されます。
bundle exec ruwi dev
Ruwi の詳細
Ruwi の詳細な機能やデプロイ方法に関しては Ruwi リポジトリの Readme をご参照ください。
また、以下のリポジトリでは実際に Ruwi を使ってクイズアプリを実装しています。こちらも参考にしてみてください。
GitHub - t0yohei/wasm-ruby-question-rubykaigi2024-ruwi
Ruwi の今後
Ruwi はひとまずの公開に漕ぎ着けましたが、機能は全然足りていません。
React や Vue.js などのフロントエンドフレームワークと競う気は全くないのですが、Ruby でフロントエンドを書く分には困らないようにしたい。
もっと言えばいい感じに Ruby でフロントエンドを書けるようにしたい。
これからの機能追加として一番やりたいと思っているのは SFC(Single File Component)的な機能を入れることです(私が Vue.js 好きなので...)。
あと、Rails に簡単に乗せれるようにしたいよね、やっぱり。
ということで引き続き鋭意開発を進めていきます。
もし Ruwi を使ってくださる方がいたら、ご感想や FB など頂けるととても嬉しいです!
それでは!

