Ruby on Browser

NSEG 2024/06 フリープレゼン大会 - connpass で発表したやつをブログにも書いておく。

speakerdeck.com

ruby.wasm で作ったやつ

文字化けを復元するよ

https://tmtms.net/mojibake/

文字化けを復元したり、文字列を文字化けさせたりできる。

自分が ruby.wasm で最初に作ったページ。

前からこういうのを作りたかったんだけど、そのためにサーバーを用意するのもアレだなーと思って、静的ページにしたかったんだけど、JavaScript で実装するのはムリだと思ったので諦めてた。

Ruby は文字コード変換処理を内部に持ってるので実現できた。ruby.wasm すばらしい。

シャッフル

https://tmtms.net/shuffle.html

子供の学校の役員を決めるために作ったやつ。

「入力」に選択肢を入れて「シャッフル」を押すと、ランダムに並べ替えられたものが「結果」に並ぶ。

選択肢は URL の list パラメータで与えられる。 サーバーは無いのでどこにも記録されないんだけど、乱数のシード値が URL に書かれるので、URL を記録しておけば結果を再現できるので他の人にも共有できる。

JavaScript だと標準の乱数はシード値を指定できないので、これも ruby.wasm によってできた。

MySQL Parameters

MySQL のパラメータとかのバージョン間の差分を表示する。めずらしく実用的なやつ。

https://mysql-params.tmtms.net/statement/

もともと6年くらい前に Vue.js の練習で作ったんだけど、JavaScript いじりたくなくて放置してたのを ruby.wasm で作り直した。

Ruby にしたおかげでまた色々いじりはじめた。Vue.js に比べると動作がちょっと遅くなったけど、まあそこは気にしたら負け。待ち時間は何もないとアレなのでイルカをくるくる回すようにした。

最近の更新は https://blog.tmtms.net/entry/202405-mysql-params にも書いた。

Rabbit on Firefox

Firefox 上でスライドを表示しているときにウサギとカメを出すやつ。対応スライドは Reveal.js / Speaker Deck / PDF。

https://tmtms.net/rabbit/

Rabbit - Rubyist用プレゼンツール のインスパイアというかオマージュというかリスペクトというかパクリというか。

Rabbit 好きで 2015年くらいまで Rabbit でスライド作ってたんだけど、その後 Reveal.js でスライドを作り始めて、ウサギを表示したくて作った。

最初は JavaScript で Reveal.js のプラグインとして作ったんだけど ruby.wasm でブックマークレットで使えるように作り直した。

当たり前だけど、ブックマークレット自体は JavaScript で書かないといけない。残念。

ruby.wasm の使い方

基本的な使い方

<script type="text/ruby"> にRubyを書く。簡単。

<!DOCTYPE html>
<html>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    # ここがRubyスクリプト
    p Time.now  #=> 出力はブラウザのコンソール
    puts "Hello, world!"
  </script>
</html>

別ファイルにして読み込むこともできる。

<!DOCTYPE html>
<html>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2/dist/browser.script.iife.js"></script>
  <script type="text/ruby" src="hogehoge.rb"></script>
</html>

ブラウザに依存しないものなら、別ファイルにしておいた方がローカルでもデバッグとかできて楽。

JavaScript の機能を使う

require 'js'

JS.eval('alert("hoge")')
JS.global.alert('hoge')

JS.eval で JavaScript を実行できる。JS.global 経由で JavaScript のグローバルオブジェクト取得や関数実行ができる。

JavaScript の値を Ruby から見るとすべて JS::Object になる。

n = JS.eval('return 123')     #=> JS::Object (123)
n.typeof                      #=> "number"
s = JS.eval('return "hoge"')  #=> JS::Object ("hoge")
s.typeof                      #=> "string"

Ruby では扱いにくいので to_i や to_s 等で変換できる。

JS.eval('return 123').to_i     #=> 123
JS.eval('return "hoge"').to_s  #=> "hoge"

JS::Object#[] でプロパティ取得&設定。JS::Object#call で JavaScript のメソッド呼び出し。JS::Object#メソッド名 でも呼び出せる。

s = JS.eval('return "hoge"')  #=> JS::Object ("hoge")
s[:length]                    #=> JS::Object (4)
s.call(:charAt, 2)            #=> JS::Object ("g")
s.charAT(2)                   #=> JS::Object ("g")

JavaScript の nullundefinedJS::Object のインスタンス。Ruby で真偽値として評価すると当然真になるので注意。

JS.eval('return null')       #=> JS::Object (JS::Null)
JS.eval('return undefined')  #=> JS::Object (JS::Undefined)

!!JS::Null       #=> true
!!JS::Undefined  #=> true

JavaScript の Proimse は await で待てる。ただし、data-eval="async" の指定が必要。

<script type="text/ruby" data-eval="async">
  promise = JS.global.fetch("https://tmtms.net")  #=> JS::Object (Promise)
  resp = promise.await  #=> JS::Object (Response)
  promise = resp.text   #=> JS::Object (Promise)
  promise.await         #=> JS::Object ("<!DOCTYPE html>\n<html>\n ....")
</script>

DOM 操作

ほぼ JavaScript。

document = JS.global[:document]
hoge = document.getElementById('hoge')
fuga = document.createElement('div')
fuga[:id] = 'fuga'
hoge.appendChild(fuga)

HTML 要素にイベントを設定するには addEventListener() を使う。 HTML の onclick 等には JavaScript を呼んでしまうので Ruby では書けない(それはそう)。

<input id="b" type="button">
<script type="text/ruby">
  require 'js'
  document = JS.global[:document]
  document.getElementById('b').addEventListener('click') do |ev|
    JS.global.alert('hoge')
  end
</script>

rubyVM.eval() で Ruby を呼べるので、onclick 内で rubyVM.eval() を使う手もある。

<input id="b" type="button" onclick="rubyVM.eval('hoge')">
<script type="text/ruby">
  require 'js'
  def hoge
    JS.global.alert('hoge')
  end
</script>

require

<script type="text/ruby" src="〜.rb"> でサーバーの rb ファイルを読むことはできるけど、require でサーバーの rb を読むことはできない。 <script src> は HTTP, require はファイルシステムからの読み込みなので。

require 'js' とかができるのは、ruby.wasm の仮想ファイルシステムにあらかじめ組み込まれているから。

なので HTML で必要な rb ファイルを読んでおく。

<script type="text/ruby" src="hoge.rb"></script>
<script type="text/ruby" src="fuga.rb"></script>
<script type="text/ruby" src="piyo.rb"></script>

これはそれぞれ依存関係が無い場合はいいんだけど、たとえば hoge.rb ファイルの中で require 'fuga' と書かれているとそこで落ちてしまう。

標準ライブラリと同じような感じで require するには仮想ファイルシステム内に rb ファイルを置いた状態で ruby.wasm を作る必要がある。

でも、そういう JavaScript のフレームワークみたいなめんどくさいことはしたくないんだなー。

JS::RequireRemote.instance.load を使うと、現在のファイルからの相対位置でサーバーから rb ファイルを読むことができるようになる。

たとえば hoge.rb ファイルの中で require_relative 'fuga' と書かれていた場合、require_relative に次のようなパッチを当てればそのまま読むことができる。

require 'js/require_remote'
module Kernel
  def require_relative(path) = JS::RequireRemote.instance.load(path)
end

gem ライブラリを使いたい場合、gem を展開して rb ファイルを取り出して HTTP でアクセスできるところに置いておけばそれだけで使えるようになるので、めっちゃ便利!! (中で require を使ってる場合は require_relative に置き換える等の手間が必要になるけど)

ただ、これだと本来ファイルシステムから読み込むべき require_relative もサーバーから取り出そうとしちゃうので、両方から読めるようにするにはこんな感じのパッチにしておくといいらしい。(Ruby in Browser観察日記 その2 - @ledsun blog より。ちょっと改変。)

require 'js/require_remote'
module Kernel
  prepend Module.new {
    def require_relative(path)
      caller_path = caller_locations(1,1).first.absolute_path || ''
      dir = File.dirname(caller_path)
      file = File.absolute_path(path, dir)
      super file
    rescue LoadError
      JS::RequireRemote.instance.load(path)
    end
  }
end

まとめ

  • JavaScript でできることならできる
  • JavaScript でできなかったこともできる
  • JavaScript より遅いけど気にしたら負け
  • Ruby でフロントエンドが書けるの最高!