OpalでVirtualDOMを実装して自作フロントエンドフレームワークを作ろうとしてる話

**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として良いのか？)


コードはこんな感じです。

```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
<img width="1424" alt="スクリーンショット 2018-12-16 13.55.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/188304/3333f6f3-5d4f-ccfb-00a7-dceb94b03a17.png">


# Rubelmで現状できること

この通り、patch関数が途中のため、ステートの変更などができず、画面に何かを表示するだけですww

https://github.com/anharu2394/rubelm に詳しく書いてありますが

```ruby
gem 'opal-sprockets'
gem 'rubelm'
```
をインストールしていただき、

```ruby
require "rubelm"
require "opal-browser"
include Rubelm::Html
view = div({class: "hello"},[
    div({class:"world"}, "hello, world!")
  ])
Rubelm::main(view,$document.body)
```
こんな感じで書くと
![](https://user-images.githubusercontent.com/26423094/48298304-af610f80-e4fe-11e8-85ce-785399d58ab6.png)
できますねって感じです。