JSrb - ruby.wasm の JS を Ruby ぽく使えるようにする

ruby.wasm の JS ライブラリは JavaScript に対する薄いラッパーなので、そのままだと Ruby では使いにくいことがあるので、最近は JS を Ruby らしく使えるようにするためのライブラリを作ってそれを使ってる。

github.com

使い方

<!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>