カスタム Module を自作して『Snabbdom』を機能拡張する - Cycle.js を学ぼう #7

前回のエントリにて Cycle.js における snabbdom の概要と基本的な使い方をご紹介しました。今回はより詳しい解説に加えてカスタム Module を作成して Snabbdom の機能を拡張する方法をご紹介します。

前回のエントリはこちら。

Snabbdom の各機能は Module として細分化されている

Snabbdom には 『Module』 と呼ばれる拡張機能が備わっています。前回のエントリでご紹介した propsattrsstyleclassdataset はそれぞれ個別の Module として実装され細分化されています。これらは全て Snabbdom 公式の Module なので Cycle.js においてはデフォルトで機能していますが、不要なものは外してしまうことが可能です。指定の仕方は以下の通り。

import {StyleModule, ClassModule, PropsModule, AttrsModule} from '@cycle/dom/lib/modules';
⋮
makeDOMDriver('#app', {
    modules: [
        AttrsModule,
        ClassModule,
        DatasetModule
    ]
});

上記の例ではmakeDOMDriverの第二引数に三つだけ渡しています。この場合、使用できるのは classattrsdataset のみとなり、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 を実行すると実際に削除されるため、例えばその直前に何か処理をさせたい場合、例えばremoveEventListenerclearIntervalといった後片付け処理を行ったり、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 を実際に作ってみるとしましょう。