ジンジャー研究室

長めのつぶやき。難しいことは書きません。

脱PolymerなWeb Componentsデザパタ

はじめに

今朝話題になっていたWeb Componentsの基本的な使い方・まとめ に触発されてみる。

すごい…!私にはこんな緻密な資料は書けないorz

だけど何か書きたい。書くぞ。

そろそろWeb Componentsしたい

Web開発に革命をもたらすと噂のWeb Componentsだが、そろそろプロダクションへの導入を検討したい。 2014年12月現在、Chrome 36+で全機能が使えるので、デモ画面や開発(テスト)ツールでは既にPolyfillなしで色々出来る。積極的に遊んでみたい。

とはいえ、現状と言えばWeb Componentsに関する情報は紹介記事がほとんどで、あまりプラクティカルな領域に踏み込めていない感がある。まあそんなものは実際に使い始めればわんさか出るので、別にその時になってからでも良いのだが、せっかくなので戦略の一つや二つ練っておきたい。

もうひとつ、Google製PolyfillライブラリのPolymerについて。 少し触ってみた感じでは特に不自由もなく素晴らしいライブラリだと思うのだが、知らず知らずのうちにロックインされてしまう微妙さがある。 それには多分いくつかの要因がある。

  • Web Componentsの記事を調べに行くと一緒にPolymerを紹介される
  • Template要素に十分な機能を持たせるライブラリがPolymerにしかない
  • webcomponents.js(旧platform.js)とpolymer.jsの機能の境目が曖昧
  • polymer-elementとして流通しているライブラリを使うと伝染する

そういうわけで、「なんだか知らないけどPolymer使っとけば安心でしょ」みたいな思考放棄の果てにjQuery的世界を再現する前になんとかしたい。 PolymerもjQueryも悪くないけど。

というわけで、実際使っていく上で考えうる限りの使い方のバリエーション、留意点などをざっと並べてみようと思う。

以下ベストプラクティス集じゃないので、念のため。

4つの仕様のうち使いたいものだけを慎重に選ぶ

Web Componentsは以下の4つの仕様からなる。

  • Custom Element
  • Shadow DOM
  • Template Element
  • HTML Imports

もちろんだが、これらは全てを統合して使ってもいいし、独立して使うことも出来る。 全てが統合された理想世界については、既にPolymerの方で語られているのでここでは特に触れない。

ここからはPolyfillをどこまで信用するかという話でもあるのだが、やはり心情的にあまり危険な橋を渡りたくないので、自分のアプリにとって必要最小限の機能を使っていきたい。 (Webサイトならいざ知らず、ブラウザのバージョンアップによるデグレードに対してパッチを当てるまでに時間がかかり、しかもそれが致命的になる場合もある)

HTML Importsを使わない

まず第一歩。 勝手な推測では大多数の人の目的はCustom Element(+Shadow DOM)なのでは、と思わなくもない。 そこで、HTML Importsを使わずに普通に.jsファイルで済ましてしまうことが可能だ。

Template Elementを使わない

また、Template要素を使う必要性も必ずしもないので、ここも好みのテンプレートに置き換え可能だ。従来の使い慣れたテンプレートエンジンや、多分Virtual DOMも使える。 ES6では複数行の文字列リテラルを宣言できるので、うるさいバックスラッシュともオサラバできる。

Shadow DOMを使わない

ここはちょっと何が正しいのか判断が付かない。というのは、個人的にCSSの干渉で「そこまで」困ったことが無いので、いまいち重要性がピンと来ないというのがある。 個人的にはそれよりも、「共通のスタイルが欲しい時にCSSコンポーネント内にインライン展開するのってどうなの?」という方が気になる。(←見解があれば教えてください)

さておき、カプセル化を気にしなければShadow DOMも削ることが出来る。(Shadow DOMのPolyfill難易度は高い) 以上のように色々そぎ落としていくとCustom Elementだけで事が済む。ミニマル主義ならこれで行きたい。

非動作時の代替手段を用意する

何らかの事情でWeb Componentsが動作しない場合に、デフォルトの動きを用意するといったことも可能だ。

  • ブラウザがWeb Componentsをサポートしていない
  • ブラウザのJavaScript機能がオフになっている
  • CDNが落ちている
  • バグ

記憶によればGitHub<time is="relative-time">要素がそのような実装になっていたと思う。 通常動作時は相対時間を表示し、動作しない場合は絶対時間が表示される。私GitHub賢い思う。

Custom Elementの生成を動的にカスタマイズする

Custom Elementの扱い方として常に紹介されるやり方は、「HTML Importなりscriptタグで読み込むなりすれば後は自由に使えるよ」というものだ。ただ、それだけでは済まない場合、もっと柔軟に扱うことが可能だ。

別の実装に切り替える

まず準備として、scriptの実行時点ではdocument.registerElement()せず、何らかの関数が呼ばれた時に登録するようにする。 これによって動的に決まるコンテクストにおいて、同じインターフェイスを持つ別の実装に切り替えることが可能になる。

if(debug) {
  registerDebuggableXxxElement();
} else {
  registerXxxElement();
}

コンテクストを渡す

パラメータを渡すことも可能。

registerXxxElement(userSettings);

要素名を変える

もちろん代わりの要素名を渡すことも可能。

registerXxxElement(userSettings, 'another-name');

フォーム部品の動作に気を使う

最早あるある現象なのだが、ハイユーザビリティを謳いながらブラウザの機能を殺していく、という皮肉なことが気をつけていないと起こりかねない。 Custom Elementだからと言って調子に乗ってオレオレ実装を始めてしまうと、元々普通に出来ていた機能をどんどん奪ってしまう。その傾向は特にフォーム部品に顕著に現れる。

  • 適切なタイミングでchangeイベントを発火する
  • <select>相当の要素の場合、valueへの代入が配下のoptionに影響する
  • 上の逆
  • フォーム送信時にシリアライズされる
  • フォーカスがあたる
  • :checked擬似クラスによる選択、スタイルの変更が出来る

正直、これらを全て正しく実装することを考えると、私ならしんどい。テストも書いたがそれでもしんどい。 しかも、:checked擬似クラスを再現する方法は用意すらされていない。 手軽に機能を継承するならis属性(is="xxx")を使うのが良いだろうか。この辺りの知見がもっと欲しい。

ここで機能を奪ってしまうことによるデメリットは以下のようなものが考えられる。

  • 他のライブラリとの親和性…たとえば:checkedなどを当たり前のように利用する
  • より大掛かりなシステムへの依存…たとえばmy-formのような特別な要素と密結合する
  • 特別な使い方を覚えるための学習コスト

調べてもあまり出てこないが、この辺の議論や見解も欲しい。

Template Elementを使って画面の初期化を遅らせる

例えば、ページ内に複数のタブがあった場合、隠れている部分の初期化は後回しにしたいという要求がある。

<div id="content-a">
  <template id="tmpl-a">
    ...
    <script src="init-a.js"></script>
  </template>
</div>
<div id="content-b" style="display:none;">
  <template id="tmpl-b">
    ...
    <script src="init-b.js"></script>
  </template>
</div>

表示されるべきコンテンツの初期化が、隠れているコンテンツの初期化にブロックされてしまうのは勿体無い。 そこで、上のように隠れている#tmpl-bのアクティベートのタイミングを遅らせることによって、初期化順序を最適化することが出来る。特にtemplate中のスクリプトが重い処理であるほど有効だ(手元での検証結果)。 もちろん銀の弾丸では無いが、武器のひとつにはなる。

Vulcanizeは早めに検討する

HTML Importsを多用するとどうしてもネットワークコストが高くなるため、JavaScriptと同様にHTMLもひとつに結合したい。 そんな時にPolymerチーム製のVulcanizeというツールが必要になる。

ただしこれには条件があって、静的にURLが宣言されている場合に使用が限られる

つまり、柔軟にURLをあれこれするフレームワークをがっつり構築した後でVulcanizeの導入を検討すると泣くことになる。(しかもエラーメッセージがはっちゃけていて分かりにくい) しかも現状HTML Importsの結合ツールはこれしか無いので、完全に詰んでしまう。結合の可能性は最初から念頭に置いておきたい。

HTML Importのモジュール管理

hrefを工夫する

HTML Importを活用して、例えば、foo.htmlbar.htmlというモジュールを独立に作成して提供したいとする。 ここで、foo.htmlbar.htmlに依存しており、アプリケーションからの利用イメージは次のようであるとする。

Application => foo.html => bar.html

この場合、foo.htmlからbar.htmlへのリンクをどうしたら良いだろうか。 Polymerは次のようにしている

<link rel="import" href="../bar/bar.html"></link>

何をしているかというと、モジュールがフラットに展開されたときに相対パスが通るように記述されている。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html
│  └── bar
│      └─ bar.html

ここでひとつ問題になるのが、Bowerあるいは他のパッケージマネージャがフラットにモジュールを展開することに依存している。

│
├── bower_components
│  ├── foo
│  │   └─ foo.html -- requires bar@1.0.0
│  ├── baz
│  │   └─ baz.html -- requires bar@0.8.0
│  └── bar
│      └─ bar.html

そうすると、フラットにならない例外があった場合や別のパッケージマネージャを使いたい場合に、おそらく困った事態になる。 この辺りもコミュニティが盛り上がらないと議論が進まない気がしているし、自分もあんまり考えたくないからES6モジュールでの提供に逃げたい気持ちでいる。

菱形依存によるスクリプトの重複実行を避ける

こちらは既にちらほら話題に上がっているのだが、例えば、foo.htmlbar.htmlの両方がjQueryに依存していた場合。 具体的には次のように菱形(ダイヤモンド)の依存関係になっている場合、

Application => foo.html => jquery.js
Application => bar.html => jquery.js

普通に双方からscriptタグでjQueryを読み込むと、2度スクリプトが実行されてしまう。 これを避けるためには、jquery.jsをHTMLでラップすると良い。

Application => foo.html => jquery.html => jquery.js
Application => bar.html => jquery.html => jquery.js

こうすることで、jquery.htmlは2度目のアクセス時からは読み込み済みという扱いになり、内部で呼ばれているjquery.jsの実行も1回で済む。

その他

Resource Timing APIが、HTML Importを通じて呼び出されるサブサブリソースの情報を拾わない気がする…。

まとめ

私的見解をぶちまけてみた。まぁどれも結論は出ていない。

もうとにかく使い方のバリエーションが沢山あって、ベストプラクティス欲しい!という感想。(←Polymer使え)