こんにちは、らこです。今日はDartで仮想DOM使って遊ぼうと思います。こちらの仮想DOMライブラリの「virtual-dom」だけでMV*なビューを書くという記事に触発されてやりました。同じようなことをDartで書きます。はじめに言っておきますが、Dartで仮想DOM使うのを推す記事ではありません。
ライブラリ
仮想DOMを使うにあたってJavaScriptの「virtual-dom」と同じようにDartでは「vdom」を使います。
サンプルコード(ボタンとカウンター)
シンプルに、ボタンを押したらカウンターが更新されるビューを書きました。
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側はまずカウンターのクラスを作りました。せっかくクラスベースなので。
class Counter {
int count = 0;
increment() {
count++;
update();
}
}
Dartっぽいですね。クラスベース万歳です。
次に仮想DOMを生成する部分と更新する部分です。
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を渡してあげる感じらしいです。
最後にエントリポイントです。
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のルートになるオブジェクトで、最初にレンダリングしたあと、マウントしています。最後にクエリセレクタでカウンターのオブジェクトを拾ってイベントリスナを噛ませています。
完成形はこちら
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側からオブジェクトを登録してあげればなんとかなります。
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化して格納します。
context["counter"] = new JsObject.jsify(counter.asMap());
あとはcounter
を仮想DOMのJavaScript側で呼ぶだけです
new VElement('button', attributes:{
'onclick':'counter.increment();'
})('Increment')
さらに問題点
明らかにオブジェクトが不可逆変換され、参照も別のオブジェクトになってしまってるのでDartとJavaScript両方のオブジェクトの同期を取る必要が出てくる。
さらにJsObject化するとそこから先はDart側でスタックトレース追えなくなるのでメンテ性の観点からもこれはダメ。
解決策
諦めてもう少しレイヤーの高いライブラリを使います。
Liquid
vdomの作者がvdom使って書いた仮想DOMのUIフレームワークです。React.jsと同じようにComponentという単位で仮想DOMを管理するんですが、結構綺麗にラップしてあって、vdomとは段違いに書きやすくなってます。
公式のサンプルアプリのコードですが、Componentが自身のDOMツリーをelement
という変数で持っていて、on-*でイベントをStream
で受け取れるので、非常にいい感じに書けます。
element.onChange.matches('input').listen((e) {
...
});
黒魔術感が全くないのも好印象でしたがまだ全然使い込んでないのでそのうちちゃんと書きます。
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の生成はやっぱりかっこ悪い。
div({}, h1({}, "Hello World"));
頑張ってるのは認めるけどやっぱ違う気がする。JSXほしい(3回目)
まとめ
完全に愚痴でした。仮想DOMはJavaScriptだけのものじゃないよということをアピールしたかっただけです。ありがとうございました。