JSrb - ruby.wasm の JS を Ruby ぽく使えるようにする
ruby.wasm の JS ライブラリは JavaScript に対する薄いラッパーなので、そのままだと Ruby では使いにくいことがあるので、最近は JS を 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" src="https://cdn.jsdelivr.net/gh/tmtm/jsrb@v0.1.0/jsrb.rb"></script>
<script type="text/ruby">
...
</script>
</html>
JavaScript で次のコードを:
elements = document.querySelectorAll('div')
elements.length
elements[0].style.width
ruby.wasm の JS で書くとこうなる:
elements = JS.global[:document].querySelectorAll('div')
elements[:length] #=> 3 (JS::Object)
elements[0][:style][:width] #=> "100px" (JS::Object)
JSrb だとこう書ける:
elements = JSrb.document.query_selector_all('div')
elements.length #=> 3 (Integer)
elements[0].style.width #=> "100px" (String)
Ruby ぽい。
プロパティを [] なしで参照できる
ruby.wasm JS は、obj のプロパティを参照するには obj[:name] と書く必要がある。
JSrb だと obj.name と書ける。
ただし、プロパティと同名の関数があったらそれが呼ばれてしまうので、その場合は [] で参照する必要がある。
undefined を返すプロパティをこの形式で呼ぶと NoMethodError になってしまうので、この場合も [] で参照する必要がある。
キャメルケースのプロパティやメソッドをスネークケースで呼べる
obj.querySelectorAll('div')
↓
obj.query_selector_all('div')
div[:innerText]
↓
div.inner_text
みたいな感じ。まあこれはお好みで…。
値を Ruby で扱いやすいように変換する
ruby.wasm JS の戻り値は全部 JS::Object なんだけど、Ruby で扱うために変換するのが面倒なので、数値や文字列や配列等は Ruby の型に変換するようにした。
| JavaScript | JSrb |
|---|---|
| number | Integer or Float |
| string | String |
| null | nil |
| undefined | nil |
| Array | Array |
| Date | Time |
これら以外のオブジェクトは JSrb オブジェクト。
length プロパティと item() メソッドがあるオブジェクトは Enumerable になる
NodeList のように複数要素を持つオブジェクトの各要素を参照するには、ruby.wasm JS だとたとえばこんな風にしないといけないんだけど:
elements = JS.global[:document].querySelectorAll('div')
elements[:length].to_i.times do |i|
elements[i][:style][:color] = 'red'
end
JSrb だとこんな風に書ける:
elements = JSrb.document.query_selector_all('div')
elements.each do |element|
element.style.color = 'red'
end
便利。
その他
JSrb.window
JavaScript の window オブジェクトに対応
JSrb.global
JSrb.window と同じ
JSrb.document
JavaScript の document オブジェクトに対応
JSrb.convert
JS::Object を Ruby で扱いやすい形に変換する:
JSrb.convert(JS.eval('return 123')) #=> 123 (Integer)
JSrb.convert(JS.eval('return 123.45')) #=> 123.45 (Float)
JSrb.convert(JS.eval('return [1,2,3]')) #=> [1, 2, 3] (Array)
JSrb.convert(JS.eval('return "abc"')) #=> "abc" (String)
JSrb.convert(JS.eval('return null')) #=> nil
JSrb.convert(JS.eval('return undefined')) #=> nil
JSrb.convert(JS.eval('return new Date')) #=> 2024-07-16 17:04:41.755 UTC (Time)
JSrb.convert(JS.eval('return {a:1,b:2}')) #=> #<JSrb: [object Object]>
JSrb#to_h
JavaScript の Object を Hash に変換する:
JSrb.new(JS.eval('return {a:1,b:2}')).to_h #=> {:a=>1, :b=>2}
JSrb#js_object
JSrb がラップしている JS::Object を返す
JSrb#timeout(sec) { ... }
sec 秒後にブロックを実行する
JS.global.setTimeout と異なり、ブロック内で await も使える。
<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="jsrb.rb"></script>
<script type="text/ruby" data-eval="async">
require 'js'
def hoge = p JS.global.fetch("/").await
JS.global.setTimeout(->{hoge}, 0)
#=> Uncaught Error: /bundle/gems/js-2.6.2/lib/js.rb:86:in `await': JS::Object#await can be called only from RubyVM#evalAsync or RbValue#callAsync JS API
JSrb.timeout(0){hoge}
#=> OK!
</script>
Discussion