動機
virtual-domの良さ
Reactに代表されるようにGitHub - Matt-Esch/virtual-dom: A Virtual DOM and diffing algorithmを使うと、デザイン変更時に、JavaScriptのロジックを考えずに、HTMLとCSSを考えるだけよくなることがわかっています。
一方でvirtual-domはGitHub - hyperhype/hyperscript: Create HyperText with JavaScript.形式のAST(抽象構文木)を自分で作る必要があります。そこでReactではIntroducing JSX - Reactという新しい記法を作って、HTMLライクに記述できる様にしました。
JSXの悪さ
JSXには2つ問題があります。
- HTMLではない
- Babel · The compiler for writing next generation JavaScriptでJSXからJavaScriptに変換する必要がある
最初から、Reactを使うのであれば問題ありません。 既存のWebアプリケーションに部分的に適用する場合は、BabelとJSXの投入は大きな変更です。 JSXは、HTMLのようでHTMLではありません。HTMLからJSXに置き換える作業が必要です。 ワークフロー上、Babelを使っていなかった場合は、新規にBabelを使用する必要があります。
HTMLを入力とする
そこで、HTMLを入力としてDOMを更新する関数を作成しました。 Handlebars.js: Minimal Templating on Steroidsの様なHTMLを生成するテンプレートエンジンを使っている場合は、生成したHTMLをそのまま入力値として使えます。 テンプレートエンジンを使っていない場合でも、現在表示しているDOMを、Google Chromeの開発ツールを使ってHTML形式でコピーすることで、流用できます。 この場合は、動的に置き換えたい値をテンプレートリテラルやテンプレートエンジンを使って置き換える作業が必要です。
設計
大方針
- GitHub - inikulin/parse5: HTML parsing/serialization toolset for Node.js. WHATWG HTML Living Standard (aka HTML5)-compliant.を使ってHTMLをASTに変換
- ASTを辿ってDOMに反映
細かい知見
appendChildはツリーの帰り道で行う
DOMにNodeを追加すると、その度にNode追加後の表示要素の場所を再計算します。 Nodeを追加する際は、Nodeとその子Nodeを作成してから、Node.appendChild - Web API インターフェイス | MDNします。
ツリーを辿る順序では行きでNodeを作成し、帰りでNodeを追加します。 ロジックとしては、子Nodeの更新が終わった後に、appendChildします。
Nodeの型の変更
DOMは既存のNodeの型を別の型に変更できません。
例えばdiv
をspan
に変更できません。またElement(HTMLタグ)をTextNode(タグの中身)に変更すること、その逆もできません。
型が変わった時は、新しいNodeを作成し、Node.replaceChild - Web API インターフェイス | MDNを使って既存のNodeと入れ替えます。
boolean attributes
Nodeにはboolean attributesという属性があります。
例えば
- disabled="disabled"
- checked="checked"
です。 これらはDOM上の属性をelement.setAttribute - Web API インターフェイス | MDNで変更しても、ブラウザ上の見た目には反映されません。
Node.disabled
などの、個別のプロパティを併用する必要があります。
やっていないこと
リストへの挿入時の最適化はしていません。
li
タグなどで作ったリストに、最後尾への追加でなく、途中への挿入をした場合、それ以降の要素は全て変更したとして扱います。
経緯
Ruby でつくるRubyを読んでASTの辿り方を学んだ
わずか120行のソースコードを写経するだけで、ASTの扱い方が理解できる本です。
update-dom-tree基本戦略は、この本のASTの扱いと一緒です。 ASTを辿ると同時に評価し、DOMに反映します。
virtual-domは、差分検知と更新の二段階に分かれています。 変更を検知したNode以下を作り直し、replaceChildします。
Node.jsのスクレイピング情報を整理してHTMLのASTを得る方法を学んだ
スクレイピングのは
- HTMLの取得
- HTMLの評価
の2要素からなります。 このうちHTMLの評価ではHTMLのパースが必要です。 色々な方法があります。この時の知見からparse5を選択しました。
- pure JavaScript
- メジャーなライブラリで使われている
- 更新が活発
の3点を重視しました。
結果
デザイン変更の自由度の上がりっぷりは最高でした。
- HTMLでイメージに合ったUIを作る
- handlebarsを使ってデータとテンプレートに分ける
- update-dom-treeを使ってビュークラスを作る
- モデルクラスを作って、データ更新のAPIとイベントを作る
という手順でコンポーネントが作れます。 UIの
- 表示項目
- 表示イメージ
- データの取得方法
が固まっている時に、サクサク作れて便利です。
実装にかかった時間は、初期実装とデバッグを合わせても丸1日程度でした。 それに比較すると、デザイン変更の自由度の上昇はかなり大きな利得です。
記録
JavaScriptでDOMを作るのは一定の規模を越えると、handlebarsのようなtemplateとinnerHTML使って全書き換えする方が簡単になる。しかし、描画のたびにDOMが全部書き換えられると、デバッグコンソールを使ったスタイル調整がしにくい。HTMLの差分をしらべて、必要な部分だけを更新してほしい。
— ぎゃばん (@ledsun) 2017年11月22日
軽い気持ちで実装してみたらできた。まあVirtual DOMじゃなくてASTとDOMの差分検出ロジックだけど。完全に「Rubyでつくる Ruby」でAST操作の練習したからです! https://t.co/umhJTGCuKx
— ぎゃばん (@ledsun) 2017年11月22日
読もう!「Rubyでつくる Ruby」
Element.removeAttribute("checked")してもチェックボックスのチェックは外れないのか。
— ぎゃばん (@ledsun) 2017年11月29日
こんな個別の対応がいるなら、VirtualDOMで属性の更新てどうなってるのよ?って見たら、Element丸ごと作り直してた。https://t.co/gPgpkxwVwW
— ぎゃばん (@ledsun) 2017年11月29日
な、なるほどー
HTML的には Boolean attributes というものらしい。https://t.co/yHg57ZaLiz
— ぎゃばん (@ledsun) 2017年12月11日