JSのMVCについて考えてみた ~ その2 テンプレートエンジンの分業とパフォーマンス

この前の続き。相変わらず思いつきでつらつら書いてて図とかまともなサンプルとかない。

JSのモデルには二種類ある

フロントエンドである以上本質的にすべてビューだとも言える。
であるがゆえにあやふやにしないほうがいい。

ビューモデル

UIの状態を示す属性。選択しているタブとか、開いているダイアログとか、そういうものの状態をDOMから読むのではなく、JSとして一度確定し、その結果をビューに反映すべきだ。激しく画面を組み替える場合はビューというグローバル変数はどこからも汚染される可能性がある。

データベースのローカルキャッシュ

たとえば、a地点からb地点の距離をユークリッド距離を求めるのに、わざわざサーバーに問い合わせるのは無駄。普通に三平方の定理で計算すればいい。アクション性が高いものほど、ここの振る舞いは分厚くなる。いわゆるHTML5アプリはここを重点的にやるほどサーバーの負担が減り、サーバーレスポンスを必要としないことで、表現的な自由度も高くなる。
基本的な考え方は「サーバーで起こってるであろうことをシミュレーションする」。この場合でも、定期的にサーバーにデータを問い合わせて同期をとる必要がある。
このレイヤーは、nodeで汎用的に書いて同じコードが使えたら嬉しかったりする。

クライアントサイドにおけるテンプレートエンジン

「クライアントのテンプレートエンジンの高速化」は、サーバーサイドにおけるテンプレーティングの速度とは全く別の軸で考える必要がある。サーバーサイドでは純粋に文字列操作だが、クライアントにおけるテンプレートエンジンはDOM生成コストが絡んでくる。Mustacheであろうがhaml.jsであろうが、結局のボトルネックはDOM生成部分になる。ネットワークIOの前では動的言語と性的言語の速度差が吹き飛ぶのと同じだ。

もっとも高速なのは、JSの変数に突っ込んでキャッシュしたDOMを直接操作するパターン。だが、よっぽど注意しないとメンテナンスが不可能なコードになる。経験上、自分以外が書いた、id探してデータを突っ込む以上のDOM操作は、まともに読めた試しがない。jQueryのDOM操作が三行以上続いてると思考停止する人も多いだろう。自分もそうだ。

ビューバインド付きのMVCフレームワーク

KnockoutとかAngularとかのこと。
経験として、開発効率を優先すると大きなビューになり、パフォーマンスを求めるとビュー(テンプレート)が分割されていき管理が難しくなる。これもトレードオフ。たとえば、パラメータ一つだけを書き換えたいのに、それが大きな画面に属していた場合はその全部を再描画する必要があったりするケースがある。関連するモデル粒度だったり、ビューの粒度を適切に分割するのは至難の業で、特にプログラミング初期はとくに見積もれないケースが多い。

誰がテンプレートを書くか?

こんな定義があったとして

//ステータス画面更新規約
var Status = {
  hp: 100,
  mp: 25
}

Statusによって展開されるMustacheテンプレート

<div id='hp'>{{hp}}hp</div>
<div id='mp'>{{mp}}mp</div>

これはJSヘヴィなアプリの場合、JSエンジニアが書くべきか、マークアップが書くべきか。
iterableだったり分岐が激しいテンプレートな場合、マークアップ側に負担が大きくなる。この傾向は、Angularのようなリッチなフレームワークを使うとより顕著になる。
フロントエンドはどうしてもマークアップドリブンになりがちだが、画面設計の際に必要なパラメータの洗い出しはJSを書く人も参加しておいた方がいい。テンプレートを展開するパラメータをマークアップ側が用意できるのが理想だが、ロジックを知らないとすぐに出せるデータ、出せないデータの区別ができず、片方だけでは苦しい。うまくコミュニケーションして連携するしかない。

リスナーイベントはプロシージャルな名前であるべき


ただ、ここで問題になるのはDOMに貼られたリスナーイベントをどう扱うかだ。
id名をRPCと捉えてDOMからは極力情報を受け取らない、という方向性が望ましいと思う。
HTML5のリッチなモデルレイヤーをもったJSは、ほとんどビューモデルとモデルが使えれば現在の状態は確定できるはずだし、またそうではない場合そうなるようにする必要があると思う。

ボタンを押したから、「ここでDOM上の数値が10だから…」とjQueryでDOMを読みに行ってはいけない。簡単なDOMならいいが、DOMが情報を持つとグローバル変数のように扱われててどんどんメンテできなくなっていく。

HTML/DOMはグローバル変数だ。可能な限り状態を持つべきではない。リスナーイベントのid/class名はプロシージャルであるべきだ。

$('#button_add_hp').on 'click', => @status.add_hp()

たとえばこんな風に