2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ruby.wasm 用のフロントエンドフレームワーク Ruwi を作った

2
Last updated at Posted at 2025-12-23

はじめに

この記事は、Ruby/Rails Advent Calendar 2025 シリーズ 2、23日目の記事です。

こんにちは、@t0yohei です。

みなさん、ruby.wasm は使っていますか?
Ruby でフロントエンドを書けるなんて、なんて良い時代でしょうか。

そんな恵まれた時代にありつつも、あるようでなかった ruby.wasm 用のフロントエンドフレームワークを作ってみたので、是非とも紹介させてください。

Ruwi(RubyWasmUi) とは

Ruwi は ruwi.wasm 用のフロントエンドフレームワークです。
今風のフロントエンドフレームワークとして、以下の思想を反映させたフレームワークになっています。

  • 宣言的 UI
  • コンポーネント指向
  • .......

今風と言うにはまだまだ色々足りないですね...。
これからに期待ということで...🙏

Ruwi ができること

じゃあ Ruwi で実際どんなことができるのか。
コードを見た方がわかりやすいと思うので、サンプルコードを見てみましょう。

index.html
<!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>
app.rb
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)

hello_gif.gif

html ファイルを用意しておいて、コンポーネントを作成。そのコンポーネントを html ファイルの特定の要素にマウントする、といういつものあれです。

React や Vue.js などのフロントエンドフレームワークに馴染みがある方は、なんとなくコードの意味が理解できるかと思います。
というよりも大体それらをパクって作っているので、それはそうという感じです。

コンポーネントは、もちろん一度定義した後に何度でも使い回ることができます。
次の例では ButtonComponent を定義して CounterComponent で複数回使い回しています。

app.rb
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)

count_gif.gif

当たり前のように思えることが当たり前にできて嬉しい!!

Ruwi を Gem として利用する

これまでの実装は Ruwi を CDN 経由で利用する場合の例です。
簡単なアプリケーションではそれで十分なのですが、もう少し複雑なアプリケーションを作りたくなった場合は、Ruwi を Gem として利用することもできます。

Ruwi を Gem として利用する場合は、実装した ruby ファイルを wasm ファイルに pack することになります。
React や Vue.js を JavaScript にビルドして使うみたいなのと一緒ですね。

セットアップ

  1. ruwiGemfile に追加する
# frozen_string_literal: true

source "https://rubygems.org"

gem "ruwi"
  1. gem をインストール
bundle install

Ruwi アプリケーションの作成

プロジェクトを初期化(初回のみ実施)

bundle exec ruwi setup

setup コマンドによりプロジェクトの構成は以下のようになります。

my-app/
├── Gemfile
└── src/
    ├── index.rb
    └── index.html

index.rb, index.html にはボイラープレートとして以下のコードが実装されています。

src/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>
src/index.rb
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 など頂けるととても嬉しいです!

それでは!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?