軽量・高速な仮想 DOM ライブラリ『Snabbdom』を使いこなす ( 基本編 ) - Cycle.js を学ぼう #6

軽量・高速な仮想 DOM ライブラリ『Snabbdom』を使いこなす - Cycle.js を学ぼう #6

HTML は DOM ツリーという JavaScript API から生成されます。そして仮想 DOM はこの DOM ツリーを表現するためのデータ構造です。DOM ツリーを直接操作して View ( HTML ) の構造を更新するよりもずっと低コストで高速であることから、ここ数年は普及の一途を辿っています。

Cycle.js は Drivers による DOM 操作を除き、原則として View を仮想 DOM で管理します。様々な仮想 DOM ライブラリがありますが、Cycle.js は軽量かつ高速であることに定評のある1)オレ調べ Snabbdom を採用 ( 依存 ) しています。今回はこの Snabbdom を Cycle.js 上で扱う方法をご紹介します。

Snabbdom とは?

シンプル・モジュール性・軽量・ハイパフォーマンスを売りとした仮想 DOM ライブラリです。Cycle.js の DOM 操作モジュールである @cycle/dom はこの Snabbdom を内包したものであり、xstream ( or RxJS ) と共に Cycle.js を支える非常に重要な柱と言えます。

ちなみに [ Snabb ] とはスウェーデン語で『速い』という意味だそうです。ですので Snabbdom を日本語に訳すと『爆速 DOM』といったところでしょうか。

HyperScript 風の文法で仮想DOMツリーを記述する

Snabbdom は HyperScript とほぼ同じ書き方で仮想DOMツリーを記述します。

注 ) 記述形式といいますか設計思想を踏襲しているのであって、ライブラリ依存しているわけではありません。

const vnode =  h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
  h('span', {style: {'font-weight': 'normal', 'font-style': 'italic'}}, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', {props: {href: '/bar'}}, 'I will take you places!')
]);

このように記述すると最終的に以下のような HTML 構造として出力されます。

<div id="container" class="two classes" onclick="anotherEventHandler">
  <span style="font-weight: normal; font-style: italic;">This is now italic type</span>
  and this is still just normal text
  <a href="/bar">I will take you places!</a>
</div>

HyperScript は h() という関数です。h() は第一引数にタグ名を含むセレクタを指定し、第二引数に属性、第三引数に子要素を指定します。但し、第二・第三は共にオプショナルとなっており、属性の指定が不要の場合はそれを省略して子要素を第二引数として記述することも出来ます。Snabbdom もこの形式をそのまま採用しています。文章で説明すると何のことやらですが、Snabbdom の TypeScript 型定義ファイルを見てみるとその仕組みがよくわかります。

import { VNode, VNodeData } from './vnode';
export declare function h(sel: string): VNode;
export declare function h(sel: string, data: VNodeData): VNode;
export declare function h(sel: string, text: string): VNode;
export declare function h(sel: string, children: Array<VNode>): VNode;
export declare function h(sel: string, data: VNodeData, text: string): VNode;
export declare function h(sel: string, data: VNodeData, children: Array<VNode>): VNode;
export default h;

第二引数に VNodeData 型の引数を渡すと属性として展開されます。代わりに文字列型を渡すと textNode 要素として、VNodeの配列を渡すと子要素として展開されます。属性と子要素 ( or textNode要素 )の両方を指定したい場合は、第二引数に属性、第三引数に子要素を渡します。

Cycle.js では全ての HTML タグ名がメソッド名として用意されている

ピュアな Snabbdom はh()関数のみを使い、タグ名までを第一引数に含めますが、Cycle.js は h() とは別に現行の HTML5 要素全てをメソッド名として独自に用意しています ( シンタックスシュガー ) 。

// 両者とも <div id="main" class="container"></div> を生成する
h('div#main.container');  // Snabbdom
div('#main.container');   // Cycle.js
// 両者とも <span style="font-weight: normal;">hello Snabbdom!</span> を生成する
h('span', {style: {'font-weight': 'normal'}}, 'hello Snabbdom!');  // Snabbdom
span('', {style: {'font-weight': 'normal'}}, 'hello Snabbdom!');   // Cycle.js

animate()rect()といった SVG 関連の要素はもちろん、meta()link()script()といったメタ要素のものも用意されています。

属性はそれぞれハッシュで指定する

例えばよく使うであろう propsattrsstyleclassdataset は以下の形式で値を指定します。

{
    props: {
        href: '/foo'
    },
    attrs: {
        type: 'text',
        width: 100
    },
    dataset: {
        action: 'reset'
    },
    class: {
        'modifiered-style': boolean
    },
    style: {
        'background-color': 'red',
        'font-weight': 'bold'
    },
}

props は DOM 要素のプロパティを指定するためのブロックです。typehrefsrcwidthheight といった項目ですね。 attrs も props とほぼ同じですが、こちらは内部で setAttribute / removeAttribute を使っています。厳密に書くのであれば使い分けるのもありですが、僕は attrs だけを使うようにしています。data 属性も 'data-action': 'reset' のようにして attrs ブロック内に含められますが、こちらに関しては別途用意されているdatasetを使用するのがよろしいでしょう。

classブロックは動的に管理したいクラスを記述します。管理するクラス名をキーとし、値が true となるとそれが適用されます。

インラインスタイルは style 配下のブロックに記述します。

style には delayed と remove という独自機能がある

style には従来の HTML / CSS インラインスタイルには無い独自の機能が実装されています。

h('span', {
  style: {
      opacity: '0',
      transition: 'opacity 1s',
      delayed: {
          opacity: '1'
      }
  }
}, 'Imma fade right in!');

delayed は 仮想DOM要素のプロパティを遅延指定ることが出来ます。上記の例では、 span 要素はまず opacity: 0 の状態でレンダリングされてから opacity: 1 になります。CSS アニメーション ( transition: 'opacity 1s' ) が指定されているので、フェードインしながら表示されるように見えます。

h('span', {
  style: {
      opacity: '1',
      transition: 'opacity 1s',
      remove: {
          opacity: '0'
      }
  }
}, 'It\'s better to fade out than to burn away');

remove は delayed の逆です。対象の仮想DOMが削除される直前に remove ブロック内に指定されたスタイルが適用されます。上記の例ではフェードアウトした後で削除されます。

締め

以上、 Snabbdom の簡単な概要と Cycle.js における使い方をご紹介しました。これだけですと『単なる軽量・シンプルな仮想DOMライブラリ』で終わってしまいますが、Snabbdom には容易にその機能を拡張することが出来る Module / Hookというものが備わっています。これを使いこなすことによって、Snabbdom ないし Cycle.js を更なる高機能なものにチューンナップすることが出来ます。次回はその Module と Hook について詳しくご紹介します。

脚注

脚注
1 オレ調べ