Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Ruby on Browser - RubyWorld Conference 2024

Ruby on Browser - RubyWorld Conference 2024

とみたまさひろ

December 05, 2024
Tweet

More Decks by とみたまさひろ

Other Decks in Technology

Transcript

  1. 自己紹介 • とみたまさひろ ▪ ▪ • 長野県北部在住 • 9年ぶりに登壇 •

    趣味 Ruby / MySQL / メール / 文字化け • 勤務先 株式会社SmartHR https://twitter.com/tmtms https://tmtms.net 2
  2. ruby-mysql • Cで書くのがいやになったんでlibmysqlclientを 使わずに全部Rubyで作り直したやつ • コンパイル不要 • だけど遅い • 最近はみんなmysql2使ってるし

    • 誰も使わない • 自分でも使ってない • と思ったけど稀に使ってる人がいるっぽい https://gitlab.com/tmtms/ruby-mysql 6
  3. LSP Router • LSP サーバーとして Rubocop LSP と Solargprah を一緒に使いたい

    • Emacs は LSP サーバーをひとつしか使えなので作った • 各 LSP サーバーの Capability に応じてリクエストを振り分ける • 便利に使ってる https://gitlab.com/tmtms/lsp_router server :rubocop do command 'rubocop --lsp 2> /tmp/rubocop.err' end server :solargraph do command 'solargraph stdio 2> /tmp/solargraph.err' end 9
  4. mrubyudf MySQLのユーザー定義関数をRubyで書けるやつ • できたら面白いかなーと思って作ってみたらできた • 使ってない https://github.com/tmtm/mrubyudf FIXNUM_MAX = 2**62-1

    def fib(n) a, b = 1, 0 n.times { a, b = b, a + b } raise 'Overflow' if b > FIXNUM_MAX b end % mrubyudf fib.spec % cp fib.so $(mysql_config --plugindir) % mysql -uroot mysql> create function fib returns int soname 'fib.so'; mysql> select fib(10); +---------+ | fib(10) | +---------+ | 55 | +---------+ 12
  5. Rabbit on Firefox • Rabbit( ) パクリ インスパイア • Firefox

    で Reveal.js / PDF / Speaker Deck のスライドを表示しているときにブック マークレットを実行するとウサギが表示される • もともと JavaScript で作ったやつを作り直した ブックマークレットはJavaScriptで書かないといけない。残念。 https://tmtms.net/rabbit/ https://rabbit-shocker.org javascript:(()=>{if(typeof rubyVM!='undefined'){rubyVM.eval('start');return}; var d=document;var h=d.getElementsByTagName('head')[0];var s=d.createElement('script'); s.src='https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser.script.iife.js'; h.appendChild(s);s=d.createElement('script');s.src='https://tmtms.net/rabbit/rabbit.rb'; s.type='text/ruby';h.appendChild(s);})() 24
  6. HTML内でruby.wasmを読み込む <script src="https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser.script.iife.js"> </script> <!DOCTYPE html> 1 <html> 2 3

    4 <script type="text/ruby"> 5 # ここがRubyスクリプト 6 p Time.now #=> 出力はブラウザのコンソール 7 puts "Hello, world!" 8 </script> 9 </html> 10 27
  7. <script type="text/ruby">にRubyを書く <script type="text/ruby"> # ここがRubyスクリプト p Time.now #=> 出力はブラウザのコンソール

    puts "Hello, world!" </script> <!DOCTYPE html> 1 <html> 2 <script src="https://cdn.jsdelivr.net/npm/@ruby/[email protected]/dist/browser.script.iife.js"> 3 </script> 4 5 6 7 8 9 </html> 10 28
  8. JSライブラリ RubyからJavaScriptを呼ぶための薄いラッパーが用意されてる • JS.eval で JavaScript を実行できる • JS.global 経由で

    JavaScript のグローバルオブジェクト取得や関数実行ができる require 'js' JS.eval('alert("hoge")') # JavaScriptを文字列で渡す JS.global.alert('hoge') # メソッドでJavaScript関数を呼ぶ 32
  9. JavaScript の値を Ruby から見るとすべて JS::Object Ruby では扱いにくいので to_i や to_s

    等で変換できる n = JS.eval('return 123') #=> JS::Object (123) n.typeof #=> "number" s = JS.eval('return "hoge"') #=> JS::Object ("hoge") s.typeof #=> "string" JS.eval('return 123').to_i #=> 123 JS.eval('return "hoge"').to_s #=> "hoge" 33
  10. • JS::Object#[] でプロパティ取得&設定 • JS::Object#call(func) で JavaScript の関数呼び出し • JS::Object#func()

    でも呼び出せる 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") 34
  11. 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 35
  12. Promise / await JavaScript の Proimse は await で待てる data-eval="async"

    の指定が必要 <script type="text/ruby" data-eval="async" src="hoge.rb"></script> 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 ....") 36
  13. HTML要素の操作 ほぼ JavaScript document = JS.global[:document] hoge = document.getElementById('hoge') fuga

    = document.createElement('div') fuga[:id] = 'fuga' hoge.appendChild(fuga) 37
  14. まあでもスクリプトで addEventListener() を使うのが良さそう <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> 39
  15. JS::RequireRemote.instance.load(path) で相対パスで指定した .rb を読み込める require_relative が↑を使うようにパッチ を改変 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 https://ledsun.hatenablog.com/entry/2023/11/14/183047 43
  16. JSrbライブラリ • JSをもう少しRubyっぽく書けるようにする • 戻り値をJS::ObjectではなくRubyのオブジェクトに変換したり • Enumerable 化して each が使えたり

    • null や undefined を nil にしたり https://github.com/tmtm/jsrb # 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 45
  17. ruby.wasmを使った簡単な例 デモ <style>.hoge-color{padding:10px}</style> <div class="hoge-color"><input class="name" value="red"><input class="code"></div> <div class="hoge-color"><input

    class="name" value="blue"><input class="code"></div> <div class="hoge-color"><input class="name" value="yellow"><input class="code"></div> def color_code(color) JSrb.global.get_computed_style(color).background_color.scan(/\d+/).map{format('%02x',_1.to_i)}.join end colors = JSrb.document.query_selector_all('.hoge-color') colors.each do |color| name = color.query_selector('input.name') color.style.background_color = name.value code = color.query_selector('input.code') code.value = color_code(color) name.add_event_listener('change') do color.style.background_color = name.value code.value = color_code(color) end end 46
  18. カスタム要素(Custom Elements) <style>hoge-color{display:block;padding:10px}</style> <hoge-color color="red"></hoge-color> <hoge-color color="blue"></hoge-color> <hoge-color color="yellow"></hoge-color> //

    JavaScript注意 class HogeColor extends HTMLElement { static observedAttributes = ['color'] connectedCallback() { var name = this.appendChild(document.createElement('input')) name.value = this.getAttribute('color') name.addEventListener('change', ()=>{this.setAttribute('color', name.value)}) this.style.backgroundColor = name.value this.code = this.appendChild(document.createElement('input')) this.code.value = colorCode(this) } attributeChangedCallback(name, oldValue, newValue) { this.style.backgroundColor = newValue if (this.code) this.code.value = colorCode(this) } } customElements.define('hoge-color', HogeColor) 49
  19. CustomElement やってみたらできた (動きは同じなのでやらない) https://github.com/tmtm/custom_element class HogeColor < CustomElement self.observed_attributes =

    ['color'] def connected_callback name = append_child(JSrb.document.create_element('input')) name.value = get_attribute('color') name.add_event_listener('change'){set_attribute('color', name.value)} style.background_color = name.value @code = append_child(JSrb.document.create_element('input')) @code.value = color_code(self) end def attribute_changed_callback(name, old, new) style.background_color = new @code.value = color_code(self) if @code end end CustomElement.define('hoge-color', HogeColor) デモ 51
  20. だいたい同じ // JavaScript class HogeColor extends HTMLElement { static observedAttributes

    = ['color'] connectedCallback() { var name = this.appendChild(document.createElement('input')) name.value = this.getAttribute('color') name.addEventListener('change', ()=>{this.setAttribute('color', name.value)}) this.style.backgroundColor = name.value this.code = this.appendChild(document.createElement('input')) this.code.value = colorCode(this) } attributeChangedCallback(name, oldValue, newValue) { this.style.backgroundColor = newValue if (this.code) this.code.value = colorCode(this) } } customElements.define("hoge-color", HogeColor) # Ruby class HogeColor < CustomElement self.observed_attributes = ['color'] def connected_callback name = append_child(JSrb.document.create_element('input')) name.value = get_attribute('color') name.add_event_listener('change'){set_attribute('color', name.value)} style.background_color = name.value @code = append_child(JSrb.document.create_element('input')) @code.value = color_code(self) end def attribute_changed_callback(name, old, new) style.background_color = new @code.value = color_code(self) if @code end end CustomElement.define('hoge-color', HogeColor) 52