Ruby on Browser
NSEG 2024/06 フリープレゼン大会 - connpass で発表したやつをブログにも書いておく。
ruby.wasm で作ったやつ
文字化けを復元するよ
文字化けを復元したり、文字列を文字化けさせたりできる。
自分が 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://zenn.dev/tmtms/articles/202405-mysql-params にも書いた。
Rabbit on Firefox
Firefox 上でスライドを表示しているときにウサギとカメを出すやつ。対応スライドは Reveal.js / Speaker Deck / PDF。
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 の null
や undefined
も JS::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 でフロントエンドが書けるの最高!
Discussion