Web Componentsが変えるWeb開発の未来から、はや二年が経ちました。コミュニティでの議論やフィードバックを経て2016年現在、Web Componentsの仕様は大きくアップデートされています。先日行われたDevFest Tokyo 2016でもWeb Components 2016 & Polymer v2 と題してWeb Componentsの最近についてお話しました。
これまでGoogleを中心に策定されてきたv0の仕様を元に、新しい仕様はMozillaやAppleなどの各ブラウザベンダーの合意を改めてとりながら策定が進められています。今日はアップデートされたWeb Componentsの仕様を説明していきます。
基本概念については割愛しますが、えーじさんのShadow DOM – Web Componentsを構成する技術:Tender SurrenderとCustom Elements – Web Componentsを構成する技術: Tender Surrenderがわかりやすいので、Shadow DOMやCustom Elementsが何なのかわからない人はまずこちらを見ましょう。
Shadow DOM v1の仕様
Shadow DOMはDOMにスコープをもたらす重要な仕様です。v1においては、基本的なコンセプトはそのままにAPIが見直されています。
createShadowRoot()からattachShadow()へ
createShadowRoot() で行っていたShadow Rootの作成ですが、v1では attachShadow() を使います。関数がShadow Rootを返す点はこれまで同様です。
const div = document.querySelector('div');
const shadowRoot = div.attachShadow({
mode: 'open' // or 'close'
});
// readonly な shadowRoot プロパティの追加
console.log(div.shadowRoot);
attachShadow() は引数にオプションを取ります。v1においてShadow DOMにはOpenedなモードとClosedなモードが存在し、mode: 'open' または mode: 'close' のように指定します。
Shadow DOMをホストできるHTML要素に shadowRoot という読み取り専用のプロパティが追加されており、要素のShadow DOMはこれを通して参照します。要素が Shadow DOMをホストした時点で shadowRoot プロパティは参照を返すようになり、Shadow DOMがないうちは null です。DevToolsで document.querySelector('div').shadowRootを実行すると undefined ではなく null が(きっと)返ってきます。
OpenedなShadow DOMとClosedなShadow DOM
Shadow DOMのモードにOpenedとClosedがあることを先に述べましたが、これはShadow DOMの外の世界からアクセスできるかどうかを示します。このため、外部からアクセス不能なClosedなShadow DOMをホストする要素の shadowRoot プロパティは、null を返します。
外部からのアクセスを許すべきか封じるべきかは賛否両論があったため、このモードという形で合意形成されました。
ClosedなShadow DOMをホストする要素は第三者に編集されないので、コンポーネントを作ったときに独立性を保つことができます。ネイティブの video 要素を Chrome で閲覧すると、各種制御UIがShadow DOMで実装されていることがわかりますが、このように外部から操作されたくない場合にはClosedモードが適しているのでしょう。逆にOpenedでアクセスの余地を残せば、より柔軟にコンポーネントを利用できるはずです。
複数Shadow Rootの廃止
v0では単一の要素に対してShadow Rootを複数持つことが許可されていましたが、v1では禁止されます。そのため attachShadow() を2回以上実行すると例外が発生します。
Shadow DOMをホストできる要素が限定的に
v0ではあらゆる要素がShadow DOMをホストすることが可能でしたが、v1では限定的になります。許可が予定されているのは次の要素です。
article, aside, blockquote, body, div, footer, h1, h2, h3, h4, h5, h6, header, nav, p, section, span
これらのネイティブ要素に加えて、カスタム要素もShadow DOMをホストできます。inputや imgなどの置換要素がShadow DOMをホストできなくなるようです。
Insertion PointsからSlotsへ
カスタム要素で囲むコンテンツがどのように表示されるかは、カスタム要素に含まれる content 要素によって挿入先(Insertion Points)が決まっていました。v1では slot 要素によるSlotsに変わります。
form-container というカスタム要素を例に解説します。
<!-- v0での<content>によるInsertion Pointsの決定 -->
<template>
<style>
::content input {
background: skyblue;
}
</style>
<div>
<content select=".class-name"></content>
<content></content>
</div>
</template>
<form-container>
<input class="class-name" type="text">
<button>Button</button>
</form-container>
form-container で囲まれた input と button は、それぞれ form-container の content に挿入されます。挿入先は select 属性に指定するセレクタで決定されていました。
v1からは次のように slot に置き換わります。slot は name 属性で命名可能で、カスタム要素を利用する側から能動的に指定できるようになります。
<!-- v1での<slot>によるSlotsの決定 -->
<template>
<style>
::slotted(input) {
background: skyblue;
}
</style>
<div>
<slot name="slot-name"></slot>
<slot></slot>
</div>
</template>
<form-container>
<input slot="slot-name" type="text">
<button>Button</button>
</form-container>
に挿入された要素を参照する疑似セレクタも ::content の子孫から、 ::slotted() になっています。
さらなる詳細について
Shadow DOMのスペックエディタであるGoogleの夷藤さんのWhat’s New in Shadow DOM v1 (by examples)という記事を見ましょう。スペックに合わせて記事も随時アップデートされています。
Custom Elements v1の仕様
Custom Elementsは開発者が任意の要素を再定義可能にする、Shadow DOM同様に重要な機能です。
ES2015 classベースの要素定義とライフサイクルコールバック
Custom Elements v1ではES2015の class 記法を使った定義が推奨されます。もちろん function を使っても同等の実装は可能ですが、ブラウザのサポートも進みつつある class で書いていくのが望ましいでしょう。
<!-- v0でのカスタム要素の定義 -->
<script>
const FooElement = Object.create(HTMLElement.prototype);
FooElement.createdCallback = () => { ... };
FooElement.attachedCallback = () => { ... };
FooElement.detachedCallback = () => { ... };
FooElement.attributeChangedCallback = () => { ... };
document.registerElement('foo-element', {
prototype: FooElement
});
</script>
ライフサイクルコールバックも class の constructor() を活かしリネームされているほか、adoptedCallback() が追加されています。
<!-- v1で推奨されるclassを使ったカスタム要素の定義 -->
<script>
class FooElement extends HTMLElement {
constructor() { ... }
connectedCallback() { ... }
disconnectedCallback() { ... }
attributeChangedCallback() { ... }
adoptedCallback() { ... }
}
window.customElements.define('foo-element', FooElement);
</script>
adoptedCallback(oldDocument, newDocument) はオーナーとなるドキュメントが変わったタイミングのハンドラです。同一のカスタム要素名が追加先のコンテキストで既に登録されている場合に、フォールバックするなどのユースケースがあるようです。
window.customElements へ
先に出ていますが、カスタム要素を定義する関数も document.registerElement から customElements.define() に変更されています。 customElements はブラウザコンテキストに追加される新たなグローバルオブジェクトです。
customElements.define() の他には次のような関数が提案されています。
// <foo-element>コンストラクタを参照する
const FooElement = customElements.get('foo-element');
// <foo-element>が定義されたタイミングを取得する
customElements.whenDefined('foo-element').then(() => {
console.log('foo-element is defined');
});
カスタム要素は非同期で処理されるため、HTMLドキュメントに存在するカスタム要素の振る舞いは定義されるまでブラウザは知り得ません。そのため、ロードの遅延などと相まって意図しないガタツキなどを招きます。そのカスタム要素が他の要素をSlotsに追加するようなものであれば、影響はなおさら大きくなります。
それを防ぐには対象のカスタム要素が定義されるタイミングを知る必要があります。customElements.whenDefined() は指定のカスタム要素が定義されるタイミングをPromiseで知らせてくれます。これによって、初期表示ではカスタム要素を非表示にしておいて、ブラウザが評価可能になるタイミングでdisplay: none; を外すといった対応も可能になるでしょう。
Type Extensionの行方
button is="foo-button" のように、元の振る舞いを活かしつつ機能を拡張するType Extensionという機能がv0では検討されていました。この機能については今なお議論が続けられています。複雑な機能を持つ既存要素の振る舞いを壊さないような機能拡張の難しさへの懸念から反対意見も出ており、WebKitは実装を見送っています。行方が気になる人は引き続きウォッチしてみてください。
HTML Imports
HTML ImportsについてはMozillaが2014年に実装を見送っており、昨年(2015年)においても引き続きES6 Modulesによるリソース解決が適切に動作するかどうかを見守る姿勢を見せています。
ブラウザサポート状況
Shadow DOM v1とCustom Elements v1はChromiumにて先行実装されており、Shadow DOM v1はChrome 53とOpera 40から、Custom Elements v1はChrome 54とOpera 41から利用可能です。
またWebKitでも先行して進められており、Shadow DOM v1がSafari 10に実装されている他、Custom Elements v1もSafari Technology Preview 14に実装されており、メニューのDevelop → Experimental Features → Custom Elementsから有効化できます。
これらが意味するところは大きく、モバイルプラットフォームで大きなシェアを占めるSafariでサポートされることで、モバイルをターゲットとしているプロダクトではWeb Componentsの利用がかなり現実的になってきたと言えます。
デスクトップブラウザについてはFirefoxとEdgeで実装が進むを期待したいところです。EdgeはDeveloper FeedbackでCustom ElementsとShadow DOMに投票しておくと、早く実装してくれるかもしれません 🙂