Alex RussellがFronteers Conference 2011で初めて発表したWeb Componentsは、長きにわたり開発者の注目を集めてきました。その概念はコミュニティに衝撃を与え、発表以来、講演や議論のテーマとして多く取り上げられています。
2013年Googleは、Web Componentsをベースとするフレームワーク、Polymer をリリースしました。その目的は、新規APIの動作を簡易的にチェックし、コミュニティからフィードバックをもらい、さらなる資金や評価を得ることでした。
導入から4年が経った今、Web Componentsは十分に普及しているはずです。ところが実際は、”あるバージョン”のWeb Componentsに対応したブラウザはChromeしかないという現状です。Polyfillがあっても、大半のブラウザでサポートしない限り、Web Componentsはコミュニティに十分に受け入れてもらえないことがよく分かります。
なぜ、こんなに時間を要するのか?
簡単に言うと、ベンダの合意を得られなかったからです。
Web Componentsを作り上げたGoogleは、それを世に出す前に、他のブラウザベンダとほとんど話し合いをしませんでした。大抵の交渉事がそうであるように、自分が当事者であると感じられない場合、人は意欲に欠け、賛成しない傾向にあります。
Web Componentsは野心的な提案でした。初期のAPIは高水準で、(正当な理由はあるにせよ)実装するのに手間がかかるものでした。そのため、ベンダと意見が合わず、論争を招く結果に終わったのです。
それでもGoogleは方向性を変えようとはしませんでした。彼らはフィードバックを求め、最終的にはコミュニティの賛同を得たのです。しかし、他のベンダが参入するまでは、ユーザビリティ面で行き詰っていたことが後になって分かりました。
Web Componentsは理論上、Polyfillがあれば、実装実績のないブラウザ上でも動くとされていました。しかし、それらが”製品に適している”と受け止められたことは一度もありません。
一方でMicrosoftは、(完成間近の)Edgeの作業に追われて、新たなDOM APIを次々と追加するような状況にはありませんでした。Appleはと言うと、Safari用の代替機能にずっと注力しています。
カスタム要素
Web Componentsにまつわる技術の中で、最も異論が少ないのが、カスタム要素です。UIパーツの見せ方や振る舞いを定義することができ、それをクロスブラウザやクロスフレームワークに配布できる点については、一般的に評価されています。
“アップグレード”
“アップグレード”という用語は、古い簡素なHTMLElement
を、定義されたライフサイクルとprototype
によって、美しいカスタム要素に作り変えることを意味します。現在は、要素がアップグレードされる際にはcreatedCallback
が呼び出されます。
var proto = Object.create(HTMLElement.prototype); proto.createdCallback = function() { ... }; document.registerElement('x-foo', { prototype: proto });
現時点で複数のベンダから5つの提案が挙がっています。そのうちの2つは、非常に期待できるものとして注目されています。
“DMITRY”
ES6クラスでうまく機能するcreatedCallback
のパターンを進化させたバージョンです。createdCallback
の概念は引き継がれていますが、サブクラス化は、より従来型に近くなっています。
class MyEl extends HTMLElement { createdCallback() { ... } } document.registerElement("my-el", MyEl);
現状の実装のように、まずカスタム要素はHTMLUnknownElement
として生成されます。その後、このプロトタイプは登録済みのプロトタイプと入れ替えられ(”スウィズル”され)、createdCallback
が呼び出されます。
このアプローチの難点は、プラットフォーム自体の振る舞いとは動きが異なるということです。最初の段階では”未知”だった要素が、ある時点で最終形に変換されるため、開発者の混乱を招くのです。
同期型のコンストラクタ
生成されたカスタム要素がツリーに挿入された時点で、パーサは開発者が登録したコンストラクタを呼び出します。
class MyEl extends HTMLElement { constructor() { ... } } document.registerElement("my-el", MyEl);
これは一見理にかなっているようですが、実際は違います。registerElement
の定義を含むスクリプトが非同期的にロードされていると、 最初にダウンロードしたドキュメントにある全てのカスタム要素が、アップグレードに失敗してしまいます。非同期型のES6モジュールに移行していく上で、これは好ましくないことです。
さらに同期型のコンストラクタには、.cloneNode()
に関連するプラットフォームの問題がついて回ります。
2015年7月のベンダ間の会合で、方向性が決定されることになっています。
is=””
開発者はis
属性によって、標準でビルトインされている要素に、カスタム要素の挙動を加えることができます。
<input type="text" is="my-text-input">
賛成意見
-
プリミティブとして公開されない要素(アクセシビリティ特性や
<form>>
コントロール、<template>
など)のビルトイン機能を拡張できる。 -
要素を”徐々に進化させる”ことができ、JavaScriptなしでも機能性を保てる。
反対意見
-
構文が複雑。
-
プラットフォームにおいて、アクセシビリティの主要なプリミティブが多く欠如しているという根本的な問題を解決できない。
-
ビルトイン機能を適切に拡張する手段がないという根本的な問題を解決できない。
-
ユースケースが限られる。開発者はShadow DOMを導入した途端、全てのビルトインされているアクセシビリティ機能を失うことになる。
合意事項
カスタム要素の仕様上、is
は”欠点”だと多くの人が思っています。Googleはすでにis
を実装済みで、下位レベルのプリミティブが公開されるまでの応急処置としてis
を捉えています。そしてMozillaとAppleは今、早急にCustom Elements V1をリリースし、プラットフォームを”欠点”で汚さずに、V2でこの問題に適切に対処するのが良いと考えています。
Domenic Denicolaが率いるHTML as Custom Elementsというプロジェクトでは、プラットフォームに不足しているDOMプリミティブを明らかにするため、カスタム要素を用いて、ビルトインされているHTML要素を再構築しようと努めています。
Shadow DOM
ベンダの間で、Shadow DOMは群を抜いて多くの議論を生みました。そのため、少しでも迅速に合意に達するには、機能を”V1″と”V2″のアジェンダに分けなければなりませんでした。
分散
分散はシャドウホストの子が視覚的にホストのShadow DOM内のスロットに”投影”されるフェーズです。これは、ユーザが内部でネストするコンテンツを、コンポーネントが利用できるようにするという機能です。
現在のAPI
現在のAPIは完全な宣言型です。Shadow DOMの中では、ホストの子を視覚的に挿入したい場所を定義するのに、特別な<content>
要素を使用できます。
<content select="header"></content>
AppleもMicrosoftも、複雑化とパフォーマンス低下を心配して、このアプローチには賛同していません。
新たな必須API
顔を突き合わせて会合を行っても、宣言型APIについては合意に至りませんでした。ですからどのベンダも強制的な解決策の追求に賛同しました。
2015年の7月を締め切りとして、全4ベンダ(Microsoft、Google、Apple、Mozilla)が、この新たなAPIの仕様策定に取り組むことになりました。そして現在までに3つの提案がなされています。3つの中で最もシンプルなのは以下のようなものです。
var shadow = host.createShadowRoot({ distribute: function(nodes) { var slot = shadow.querySelector('content'); for (var i = 0; i < nodes.length; i++) { slot.add(nodes[i]); } } }); shadow.innerHTML = '<content></content>'; // Call initially ... shadow.distribute(); // then hook up to MutationObserver
主な障害は、タイミングです。ホストノードの子が変わり、MutationObserver
コールバックが呼び出された際に再分散を行うと、レイアウトプロパティの要求は誤った結果を返します。
myHost.appendChild(someElement); someElement.offsetTop; //=> old value // distribute on mutation observer callback (async) someElement.offsetTop; //=> new value
offsetTopの呼び出しを行うと、分散の前に同期したレイアウトが実行されてしまいます。
これは大した問題には見えないかもしれませんが、スクリプトとブラウザの中身が多数の異なる処理を行うためには、offsetTop
の値の正確性が鍵になります。例えば、要素をビューでスクロールして見る場合などです。
これらの問題が解決されないと、宣言型APIについての議論に逆戻りになってしまうかもしれません。この形式は、現在の<content select>
のスタイルか、新たに(Appleから)提案された“named slot”APIのどちらかになるでしょう。
新たな宣言型API ”named slot”
この”named slot”の提案は、現在の”content select”のAPIがよりシンプルになったもので、コンポーネントのユーザはこれを使って希望の分散先のスロットを持つコンテンツに分かりやすく名前をつけなければなりません。
<x‐page>のシャドウ・ルート:
<slot name="header"></slot> <slot></slot> <slot name="footer"></slot> <div>some shadow content</div>
<x-page>の用法
<x-page> <header slot="header">header</header> <footer slot="footer">footer</footer> <h1>my page title</h1> <p>my page content<p> </x-page>
構成ツリー/レンダツリー(ユーザに見えるもの)
<x-page> <header slot="header">header</header> <h1>my page title</h1> <p>my page content<p> <footer slot="footer">footer</footer> <div>some shadow content</div> </x-page>
ブラウザはシャドウホストの直接の子(myXPage.children
)を見ており、子の中に、ホストのshadowRoot
内の
マッチするものが見つかれば、ノードは対応する
賛成意見:
1.分散がより明確で理解しやすくなり、”何が起こったか分からない”という状況が改善される。
2.エンジンが処理するのに分散がよりシンプルになる。
反対意見:
1.<select>のようなビルトインの要素がどのように動作するのか説明されていない。
2.スロット属性でコンテンツを装飾するのは、ユーザの仕事が増えることにつながる。
3.表現力が低い。
“クローズド” 対 “オープン”
shadowRoot
が”クローズド”だと、 myHost.shadowRoot
を介してアクセスすることができません。これによりコンポーネントの作成者は、ユーザから実装の詳細に干渉されることがないというある程度の保証を得られます。これは、何かしらを切り分けておくのにクロージャを使うのと同じようなものです。
Appleは、ブロックする機能は重要であるという考えを強めています。実装詳細は外に露呈させるべきではないこと、また”クローズド”モードは“独立した”カスタム要素が求められる時に必要な機能になることを、主張しています。
一方Googleは、”クローズド”なシャドウ・ルートはアクセシビリティやコンポーネントを利用したユースケースの妨げになると考えています。偶然にshadowRoot
に何かしてしまうというのはあり得ないことで、意図を持って何かしようとしているならば、そこには正当な理由があるはずだと主張しています。JS/DOMは今のオープンな状態のままにしておくべきだというのが彼らの意見です。
4月の会合では、前進のためには”モード”機能が必要であるという点では一致しましたが、デフォルトを”オープン”にするのか”クローズド”にするのかという点では、各ベンダの意見が割れました。結果として、V1では”モード”が必須パラメータになるため、特定のデフォルトは必要ないということで合意に至りました。
element.createShadowRoot({ mode: 'open' }); element.createShadowRoot({ mode: 'closed' });
シャドウ・ピアシング・コンビネータ
“ピアシング・コンビネータ”は特別なCSSの”コンビネータ”で、シャドウ・ルートの中にある要素を、外側からも検索対象とすることができます。以下は/deep/を >>>:
に書き換えた例です。
.foo >>> div { color: red }
Web Componentsの仕様が最初に定められた時は、これは必要だと考えられていましたが、実際の使われ方を見ると、Web Componentsを非常に魅力的なものにするはずのスタイル境界がいとも簡単に壊れ、単に問題を引き起こしただけのようでした。
パフォーマンス
スタイルの計算については、エンジンが外部のセレクタや状態を考慮に入れる必要がなければ、緻密に調査されたShadow DOM内では非常に速いです。まさにピアシング・コンビネータの存在が、この種の最適化を禁じているのです。
代替案
シャドウ・ピアシングのコンビネータをドロップするということは、ユーザがコンポーネントの外側から見た目をカスタマイズできないということではありません。
CSSカスタムプロパティ(変数)
Firefox OSでは、特定のスタイルのプロパティを提示するために、CSSカスタムプロパティを使います。これは、外部から定義(あるいはオーバーライド)することができます。
外部(ユーザ):
x-foo { --x-foo-border-radius: 10px; }
内部(プログラム作成者):
.internal-part { border-radius: var(--x-foo-border-radius, 0); }
カスタム疑似要素
カスタム疑似セレクタを定義する能力の再導入に、複数のベンダが興味を示してきました。擬似セレクタは、与えられた内部パーツのスタイルを決めるものです(<input type=”range”>
の部分にスタイルを付ける方法と似ています)。
x-foo::my-internal-part { ... }
これはShadow DOM V2の仕様で検討される可能性が高いでしょう
Mixins – @extend
SASSの@extendをCSSで働かせるための仕様が提案されています。これはコンポーネントのシステム作成者にとっては、ユーザに特定の内部パーツを適用するためのプロパティの”袋”を提供するのに役立つはずです。
外部(ユーザ):
.x-foo-part { background-color: red; border-radius: 4px; }
内部(プログラム作成者):
.internal-part { @extend .x-foo-part; }
複数のシャドウ・ルート
同じ要素の中に、どうして複数のシャドウ・ルートが必要なのでしょうか? 皆さんは不思議に思いますよね。その答えは、継承です。
例えば、私が<x-dialog>
コンポーネントを書いているとしましょう。このコンポーネントの中では、私はマークアップ、スタイル付け、ダイアログウィンドウを開いたり閉じたりするためのインタラクションを全て書きます。
<x-dialog> <h1>My title</h1> <p>Some details</p> <button>Cancel</button> <button>OK</button> </x-dialog>
このシャドウ・ルートは、<content>
の挿入によってdiv.inner
の中にユーザに提供されたコンテンツを引き込みます。
<div class="outer"> <div class="inner"> <content></content> </div> </div>
私は、<x-dialog-alert>
も作りたいと思っています。これは見た目も機能も<x-dialog>
のようですが、より制限の多いAPIでは、 alert('foo')
に似たものになります。
<x-dialog-alert>foo</x-dialog-alert> var proto = Object.create(XDialog.prototype); proto.createdCallback = function() { XDialog.prototype.createdCallback.call(this); this.createShadowRoot(); this.shadowRoot.innerHTML = templateString; }; document.registerElement('x-dialog-alert', { prototype: proto });
新しいコンポーネントは、独自のシャドウ・ルートを持ちますが、親クラスのシャドウ・ルートの上位で機能するようにデザインされています。<shadow>
はより”古い”シャドウ・ルートを代表し、内部にあるコンテンツの投影を可能にします。
<shadow> <h1>Alert</h1> <content></content> <button>OK</button> </shadow>
一度、複数のシャドウ・ルートについて理解してしまえば、強力な概念となります。欠点は、複雑な点が多いことと、特異な状況で起こるエッジケースが多く導入されてしまうことです。
複数のシャドウなしの継承
継承は複数のシャドウ・ルートがなくても可能です。しかし、スーパークラスのシャドウ・ルートを手動で変換しなくてはいけません。
var proto = Object.create(XDialog.prototype); proto.createdCallback = function() { XDialog.prototype.createdCallback.call(this); var inner = this.shadowRoot.querySelector('.inner'); var h1 = document.createElement('h1'); h1.textContent = 'Alert'; inner.insertBefore(h1, inner.children[0]); var button = document.createElement('button'); button.textContent = 'OK'; inner.appendChild(button); ... }; document.registerElement('x-dialog-alert', { prototype: proto });
この方法の欠点は、次のようなものです。
1.エレガントではない。
2.サブコンポーネントは、スーパーコンポーネントの実装の詳細に依存する。
3.スーパーコンポーネントのシャドウ・ルートが”クローズド”だった場合、this.shadowRoot
がundefined
となるため使えない。
HTML Imports
HTML Importsは、.htmlドキュメントで定義される全てのアセットを、他のスコープにインポートする方法を提供します。
<link rel="import" href="/path/to/imports/stuff.html">
以前お伝えしたように、Mozillaは今のところ、HTML Importsを実装する予定はありません。これは、外部のアセットを導入するための方法を取り入れる前に、ES6のモジュールがうまくいくかどうかを確かめたいからでもあり、今以上にできることが大幅に増えるとは感じていないからでもあります。
私たちはこの1年以上、Firefox OSにおけるWeb Componentsを研究してきました。その結果、依存関係の解決や、普通の<script>
タグでロードすることによる要素の登録は、既存のモジュール構文(AMDあるいはCommon JS)を使うことで十分成し遂げられると分かりました。
HTML Importsは、より古い<element>
やPolymerの進行中の登録の構文など、より単純、あるいはより宣言的なワークフローに自身をうまく加えます。
この単純化は、Importsが独立したマネジメントの解決法としてきちんと受け取られるのに十分なコントロールを提供していないため、コミュニティからの批判となります。
数ヶ月前に決定がなされる前に、Mozillaにはフラグの裏で機能している実装がありましたが、不完全な仕様に苦労していました。
今後、どうなるのか?
AppleのIsolated Custom Elementsの提案は、documentのスコープ内に自身のカスタム要素を提供するために、HTML Importsスタイルのアプローチを利用します。おそらく、そこに未来はあるでしょう。
Mozillaでは、カスタム要素の定義のインポートが、間もなく登場するES6モジュールAPIとどのように提携するかを模索したいのです。これにより、現在できないことを開発者ができるようになるなら、その時には私たちは実装する準備はできています。
まとめ
Web Componentsは、今日のブラウザに大きな機能を加えるのがどれほど難しいかを表すいい例です。追加された全てのAPIは不明瞭な状態で生きており、身近な障害として残っています。
からまって巨大なボールになってしまったヒモをほぐそうとして、さらにからまるようなものです。このからまり、つまりプラットフォームは、より大きく、より複雑に成長しています。
Web Componentsの開発には3年以上がかかっていますが、まもなく終わりが来るのではないかと、我々は楽観視しています。主要なベンダは全て仕事を続けていますし、熱心で、残った問題を解決するのにかなりの時間を投資しています。
Webのコンポーネント化に備えていきましょう。
さらに
- public-webappsのメーリングリストで、進行中の議論に参加しましょう。
- W3C Web Components Repoに注目しておきましょう。
- ニュースレターWeb Components Weeklyに登録しましょう。
- about:configで‘dom.webcomponents.enabled’ prefを実行させることにより、現在のFirefoxでWeb Componentsを使って遊んでみましょう。