カスタム Module を自作して『Snabbdom』を機能拡張する - Cycle.js を学ぼう #7
wakamsha
前回のエントリにて Cycle.js における snabbdom の概要と基本的な使い方をご紹介しました。今回はより詳しい解説に加えてカスタム Module を作成して Snabbdom の機能を拡張する方法をご紹介します。
前回のエントリはこちら。
Snabbdom の各機能は Module として細分化されている
Snabbdom には 『Module』 と呼ばれる拡張機能が備わっています。前回のエントリでご紹介した props
、 attrs
、 style
、 class
、 dataset
はそれぞれ個別の Module として実装され細分化されています。これらは全て Snabbdom 公式の Module なので Cycle.js においてはデフォルトで機能していますが、不要なものは外してしまうことが可能です。指定の仕方は以下の通り。
import {StyleModule, ClassModule, PropsModule, AttrsModule} from '@cycle/dom/lib/modules';
⋮
makeDOMDriver('#app', {
modules: [
AttrsModule,
ClassModule,
DatasetModule
]
});
上記の例ではmakeDOMDriver
の第二引数に三つだけ渡しています。この場合、使用できるのは class
、 attrs
、dataset
のみとなり、style
を使おうとしてもその機能が抜き取られているので何も起こらないままとなります。逆に言うとオリジナルの機能を実装した自作の Module をここに追加すると Cycle.js 内で自由に使うことが出来るということです。
DOM ノードのライフサイクルを理解する
アプリケーションが起動すると最初の DOM ノードがレンダリングされ、最終的に HTML 要素となってViewとして画面に表示されます。画面遷移が発生したり同一画面内での状態が変化すると、それまでの DOM ノードは破棄されて新しいものが生成されます。このように DOM ノードは生成と破棄、すなわち生まれては死に、生まれては死に…、という輪廻転生をひたすら繰り返します。こうした生成と破棄の循環を一般的に『ライフサイクル』と呼びます。
ライフサイクルの中で DOM ノードの状態は目まぐるしく変化し、それぞれのタイミングでメソッドが呼び出されるようになっています。
メソッド名 | 呼び出されるタイミング | コールバック引数 |
---|---|---|
pre |
パッチプロセスが始まるとき | なし |
init |
VNode が追加されるとき | vnode: VNode |
create |
VNode に基づいて DOM 要素が作られるとき | emptyVNode: VNode , vnode: VNode |
insert |
要素が DOM に挿入されるとき | vnode: VNode |
prepatch |
要素にパッチが適用されるとき | oldVnode: VNode , vnode: VNode |
update |
要素が更新されるとき | oldVnode: VNode , vnode: VNode |
postpatch |
要素にパッチが適用された後 | oldVnode: VNode , vnode: VNode |
destroy |
要素が直接 or 間接的に削除されるとき | vnode: VNode |
remove |
DOM が要素から直接削除されるとき | vnode: VNode , removeCallBack: () => void |
post |
パッチプロセスが終了したとき | なし |
[ ※ 余談 ] VNode という仮想 DOM の型について
Snabbdom ならびに Cycle.js では実際の DOM ノードよりも仮想 DOM を頻繁に扱いますが、それらはVNode
という型で定義されています。main
関数や Drivers しか書かない場合は特に意識する必要はありませんが、カスタム Module を作るとなると VNode の中身までしっかり理解しておくべきです。以下は VNode の型定義ファイルです。どういったメンバーを持っているのかを知っておくだけでも大まかな使い方がつかめるかと思います。
export interface VNode {
// CSS セレクタ
sel: string | undefined;
// VNode に持たせたい様々なプロパティ。
// カスタム時はここを上書きして自分で好きに定義して OK
data: VNodeData | undefined;
// 子要素。text と排他
children: Array<vnode | string> | undefined;
// DOM ノード
// 従来の JS にあるお馴染みの API が使える
elm: Node | undefined;
// テキストノード。children と排他
text: string | undefined;
// VNode を識別するユニークキー
// Key は string or number と定義されている
key: Key;
}
key プロパティを使って VNode のユニーク性を担保する
Snabbdom はパフォーマンスを高速に保つために仮想 DOM ノードの生成と破棄を最小限に抑えようとします。例えばひとつの div 要素を非表示とするために破棄し、しばらくしてからまた表示したいとします。この時、一度破棄してからまた生成するのだから別の DOM ノードと思いがちですが、セレクタも子要素も全く同じだと生成・破棄の処理をしてくれない場合があります。どこか一文字でも差分があれば再生成してくれますが、全く同じだとそうは行きません。かといってセレクタなど表示内容に直接影響のないところを毎回書き換えるのはスマートではありません。
そこでkey
プロパティを使います。key は DOM ノードの ID とは全く別の 仮想 DOM ノードをユニークに識別するためのモノです。DOM ノードの構造が全く同じでも key が違えば Snabbdom は別モノと判断するため、ライフサイクルが最初から機能します。上記のようなシチュエーションに陥ったら key を活用すれば解決します。
閑話休題 ( ? ) 。DOMノードライフサイクルの中で比較的使用頻度が高いであろうフック・メソッドについて捕捉しておきます。
pre
パッチプロセスがこれから始まるとき、すなわちDOMノードの生成がこれから始まるぞというタイミングで実行されます。コールバック引数が何もないことから DOM ノードはもちろん 仮想DOMノードの参照は出来ません。変数の初期化などといった処理はこのタイミングで行います。
create
仮想DOMノードから生成されたDOMノードを参照することが出来ます。生成された DOM ノードは vnode.elm
で参照することが出来ます。生成が完了して画面に表示される直前にDOMノードを直接操作したいときはこのタイミングで行います。
insert
生成された DOM ノードが document
に挿入され、パッチプロセスが終了したタイミングで実行されます。このタイミングでDOMノードのサイズや座標が確定するため、getBoundingClientRect()
などで値を取得することも可能です。
remove
DOM ノードが削除されるタイミングで実行されます。第二引数の removeCallBack
を実行すると実際に削除されるため、例えばその直前に何か処理をさせたい場合、例えばremoveEventListener
、clearInterval
といった後片付け処理を行ったり、DOMノードの削除自体を遅らせたいときなどに使います。
destroy
remove と似ていますが、remove は自身が直接削除されるときしか実行されません。対する destroy は親 DOM ノードが削除される際に引っ張られる形で削除されるときも実行されます。
Module の全体適用と部分適用について
Snabbdom の Module は基本的に DOM ノード全体に適用されるようになっています。デフォルトで用意されている ClassModule や AttrsModule といった Module は全ての DOM ノードで使えるべきですよね?しかしカスタム Module を作りたいとなったとき、全体ではなくある特定の 仮想 DOM ノードに対してのみ適用といったケースも考えられます。Snabbdom には Hook と呼ばれる機能が備わっており、カスタム Module を Hook として使うことで部分適用を実現することが出来ます ( ※ 使い方は後述 ) 。
また、Module は全体適用とするか部分適用とするかによってフック・メソッドのうち利用出来るものが違ってきます。
全体適用 | 部分適用 | |
---|---|---|
pre |
● | |
init |
● | |
create |
● | ● |
insert |
● | |
prepatch |
● | |
update |
● | ● |
postpatch |
● | |
destroy |
● | ● |
remove |
● | ● |
post |
● |
以上、これらをふまえて全体適用 / 部分適用それぞれのカスタム Module を実際に作ってみるとしましょう。