Opal (Ruby-to-JavaScript compiler) Advent Calendar 2018 16日の記事です
最近のフロントエンドでは、React.js, Vue.js, Angular.js, Riot.js, Elm, Hyperapp.....などなど多様なフレームワークが使われていますね。
このようなフレームワークはVirtualDOMという技術があってこそなんです。
私は、VirtualDOMに興味が出たことや300行で実装されてるHyperappのコードを見たりして、自分でVirtualDOMを実装したいという気持ちが湧いてきました。
そして、自作のフロントエンドフレームワークを作って見たいなぁと思いました。
VirtualDOMって何?
JavaScriptから効率よくDOMを扱ってよりモダンな動きを実現できる。
Webでの流れは大体このようになっている
①Webに表示するためにVDOMをRealDOMへ
VDOMはJavaScriptのObjectである。
このようなVDOMがあれば
{
"nodeName": "div",
"attributes": {},
"children": []
}
下のようなHTMLが吐かれるだろう。
<div></div>
②ステートの変更があれば、変更する部分の差分をとって、RealDOMへ
という感じですね。(ざっくりしすぎた)
詳しい説明は
https://postd.cc/the-inner-workings-of-virtual-dom/
が参考になりました。
OpalでVirtualDOMを実装しようとしている
https://github.com/anharu2394/rubelm で開発中です。
Rubelmという名前は最初RubyでもElmっぽいことできたらいいなぁと思ったからなのですが今は別にそこはどうでもよくなりましたww今はHyperappっぽくしたいなって感じです。
Preactの実装だとVNode Classがありますが、Hyperappの実装では純粋なObjectを使っています。
なので現状Rubelmでも純粋なHashで実装してます(Rubyとして良いのか?)
コードはこんな感じです。
module Rubelm::Vdom
require "opal-browser"
def self.render(view,root)
self.create(view,root)
end
def self.create(vdom,parent)
element = Browser::DOM::Element.create(vdom[:nodeName])
vdom[:attributes].each do |name, val|
element.set(name,val)
end
vdom[:children].each do |child|
child.instance_of?(Hash) ? create(child, element) : element.text = child
end
parent << element
end
def self.recycle(ele)
attributes = {}
children = []
ele.attributes.each do |name, value|
attributes[name] = value
end
ele.children.to_ary.each do |node|
node.instance_of?(Browser::DOM::Element) ? children << recycle(node) : children << node.whole
end
node = {
nodeName: ele.name.downcase,
attributes: attributes,
children: children
}
end
# patchは実装途中
def self.patch(old_node,new_node,root)
ele = root.elements.to_ary[0]
if old_node[:nodeName] != new_node[:nodeName]
ele.remove
create(new_node,root)
elsif ele.attributes.namespace != new_node[:attributes]
ele.attributes.merge!(new_node[:attributes])
root
end
new_node[:children].each_with_index do |child, i|
child.instance_of?(Hash) ? patch(old_node[:children[i], child, ele]) : ele.text = child
end
root
end
end
createは①の役割(VDOMを画面へ表示させる)
recycleはステートが変更された時点でのRealDOMをVdomにする関数
patchはrecycleされたVDOMとステート変更後のVDOMを比較して差分を表示させる関数(まだ実装途中)
このような感じです。
Opal-rspecでTDDしてる
Rspecというテストフレームワークは誰でもご存知ですよね。
それのOpal版があるんです。
opal-rspecはブラウザーで走らせることもできるので、Vdomのテストもできます!!
重いですがw
Rubelmで現状できること
この通り、patch関数が途中のため、ステートの変更などができず、画面に何かを表示するだけですww
https://github.com/anharu2394/rubelm に詳しく書いてありますが
gem 'opal-sprockets'
gem 'rubelm'
をインストールしていただき、
require "rubelm"
require "opal-browser"
include Rubelm::Html
view = div({class: "hello"},[
div({class:"world"}, "hello, world!")
])
Rubelm::main(view,$document.body)