Dartで仮想DOMライブラリの「vdom」だけでMV*なビューを書くのは大変でした

こんにちは、らこです。今日はDartで仮想DOM使って遊ぼうと思います。こちらの[仮想DOMライブラリの「virtual-dom」だけでMV*なビューを書く](http://qiita.com/seanchas_t/items/05d70bf4e095865583cf)という記事に触発されてやりました。同じようなことをDartで書きます。はじめに言っておきますが、Dartで仮想DOM使うのを推す記事ではありません。

# ライブラリ
仮想DOMを使うにあたってJavaScriptの「virtual-dom」と同じようにDartでは「[vdom](https://github.com/localvoid/vdom)」を使います。

# サンプルコード（ボタンとカウンター)
シンプルに、ボタンを押したらカウンターが更新されるビューを書きました。

HTMLにはマウント用の要素をおいているだけです。

```html:vdom_dart.html
<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <title>Vdom-Dart</title>
    <link rel="stylesheet" href="vdom-dart.css">
  </head>
  <body>
    <h1>Vdom-Dart</h1>

    <div id="app"></div>

    <script type="application/dart" src="vdom-dart.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>
```

Dart側はまずカウンターのクラスを作りました。せっかくクラスベースなので。

```dart
class Counter {
  int count = 0;

  increment() {
    count++;
    update();
  }
}
```

Dartっぽいですね。クラスベース万歳です。
次に仮想DOMを生成する部分と更新する部分です。

```dart
VElement render() {
  return new VElement('div')(
    [
      new VElement('p')(counter.count.toString()),
      new VElement('button', id:'increment')('Increment')
    ]);
}

void update() {
  var newTree = render();
  tree.update(newTree, const Context(true));
  tree = newTree;
}
```

はい、だんだん嫌な感じがしてきますね。`VElement`はvdomが提供する仮想DOMの要素です。要素の種類はコンストラクタに文字列で渡します。JSX Harmonyを使いたいです。

`update()`の中ではdiff/patch作業をしています。`const Context(true)`は、patch作業を当てるときに「初期化作業じゃない」ならtrueを渡してあげる感じらしいです。

最後にエントリポイントです。

```dart
VElement tree;
Counter counter = new Counter();

void main() {
  tree = render();
  tree.init();
  tree.mount(document.querySelector('#app'), const Context(false));
  tree.attached();
  tree.render(const Context(true));

  document.querySelector('#increment').onClick.listen((e) => counter.increment());
}
```

`tree`が仮想DOMのルートになるオブジェクトで、最初にレンダリングしたあと、マウントしています。最後にクエリセレクタでカウンターのオブジェクトを拾ってイベントリスナを噛ませています。

完成形はこちら

```dart
import 'dart:html';
import 'package:vdom/vdom.dart';

class Counter {
  int count = 0;

  increment() {
    count++;
    update();
  }
}

VElement tree;
Counter counter = new Counter();

VElement render() {
  return new VElement('div')(
    [
      new VElement('p')(counter.count.toString()),
      new VElement('button', id:'increment')('Increment')
    ]);
}

void update() {
  var newTree = render();
  tree.update(newTree, const Context(true));
  tree = newTree;
}

void main() {
  tree = render();
  tree.init();
  tree.mount(document.querySelector('#app'), const Context(false));
  tree.attached();
  tree.render(const Context(true));

  document.querySelector('#increment').onClick.listen((e) => counter.increment());
}
```

# 問題点
最後のイベントリスナですが、クエリセレクタがnullを返した場合は当然例外を吐きます。JavaScriptの場合はvirtual-domが属性のマップに関数オブジェクトを格納できるのでいいですが、DartのvdomはVElementのコンストラクタで渡せる`attributes`が`Map<String, String>`です。文字列で`counter.increment()`とか渡してもJavaScriptじゃないので呼び出せません。vdomの実装の問題のような感じもありますが、そもそもDartがHTMLとの距離が遠いということも原因にあります。

# 無理やり解決する
仮想DOMのレンダリングの時にJavaScriptのコンテキストにDart側からオブジェクトを登録してあげればなんとかなります。

```dart
import 'dart:html';
import 'dart:js';
import 'package:vdom/vdom.dart';

class Counter {
  int count = 0;

  increment() {
    count++;
    update();
  }

  Map asMap() => {
    'count' : count,
    'increment': increment
  };
}

VElement render(Counter counter) {
  context["counter"] = new JsObject.jsify(counter.asMap());
  return new VElement('div')(
    [
      new VElement('p')(counter.count.toString()),
      new VElement('button', attributes:{
        'onclick':'counter.increment();'
      })('Increment')
    ]);
}

void update() {
  var newTree = render(counter);
  tree.update(newTree, const Context(true));
  tree = newTree;
}

VElement tree;
Counter counter;

void main() {
  counter = new Counter();
  tree = render(counter);
  tree.init();
  tree.mount(document.querySelector('#app'), const Context(false));
  tree.attached();
  tree.render(const Context(true));
}
```

まずCounterクラスにasMap関数を定義して、DartのオブジェクトをJSON互換のMapに変えます。

次に`render()`で`context`にCounterオブジェクトをMap化して格納します。

```dart
context["counter"] = new JsObject.jsify(counter.asMap());
```

あとは`counter`を仮想DOMのJavaScript側で呼ぶだけです

```dart
      new VElement('button', attributes:{
        'onclick':'counter.increment();'
      })('Increment')
```

# さらに問題点
明らかにオブジェクトが不可逆変換され、参照も別のオブジェクトになってしまってるのでDartとJavaScript両方のオブジェクトの同期を取る必要が出てくる。

さらにJsObject化するとそこから先はDart側でスタックトレース追えなくなるのでメンテ性の観点からもこれはダメ。

# 解決策
諦めてもう少しレイヤーの高いライブラリを使います。

## [Liquid](https://github.com/localvoid/liquid)
vdomの作者がvdom使って書いた仮想DOMのUIフレームワークです。React.jsと同じようにComponentという単位で仮想DOMを管理するんですが、結構綺麗にラップしてあって、vdomとは段違いに書きやすくなってます。

公式の[サンプルアプリのコード](https://github.com/localvoid/liquid/blob/master/example/todo/todo.dart)ですが、Componentが自身のDOMツリーを`element`という変数で持っていて、on-*でイベントを`Stream`で受け取れるので、非常にいい感じに書けます。

```dart
    element.onChange.matches('input').listen((e) {
      ...
    });
```

黒魔術感が全くないのも好印象でしたがまだ全然使い込んでないのでそのうちちゃんと書きます。

## [React-dart](https://github.com/cleandart/react-dart)
React.jsのDartポーティングです。React.jsでできることはできます。JSX Harmonyが無くなって、型がついたReactです。Liquidよりも洗練されてるしドキュメントもReact.jsのをだいたい流用できるので書くのに困ることはないんですが、設計的に「どうなのこれは」っていう部分があります。

例えばこれ https://github.com/cleandart/react-dart/blob/master/lib/react.dart#L19
js側にはない独自の`bind()`という関数が追加されてます。中身は`setState`してるだけです。
こういうよくわからないシンタックスシュガーを増やすのは混乱を招くだけだと思うのでやめたほうがいいと思う。

あと仮想DOMの生成はやっぱりかっこ悪い。

```dart
div({}, h1({}, "Hello World"));
```

頑張ってるのは認めるけどやっぱ違う気がする。JSXほしい（3回目)

# まとめ
完全に愚痴でした。仮想DOMはJavaScriptだけのものじゃないよということをアピールしたかっただけです。ありがとうございました。