7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

VirtualDOMAdvent Calendar 2014

Day 17

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

Last updated at Posted at 2014-12-16

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

ライブラリ

仮想DOMを使うにあたってJavaScriptの「virtual-dom」と同じようにDartでは「vdom」を使います。

サンプルコード(ボタンとカウンター)

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

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側はまずカウンターのクラスを作りました。せっかくクラスベースなので。

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のコンストラクタで渡せるattributesMap<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だけのものじゃないよということをアピールしたかっただけです。ありがとうございました。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?