我々Web開発者がWeb Componentsという言葉を耳にしてから、もう2年程経ったでしょうか。Web Componentsが変えるWeb開発の未来という記事に、「今のWeb開発がどのような課題を抱えているか、それをWeb Componentsがどう解決するか」を書きました。これを踏まえて、本連載ではWeb Componentsの仕様から実装、PolymerやX-TagといったWeb Componentsを支えるライブラリなどの周辺知識まで解説していきます。
Web Componentsを支える4つの仕様
連載第1回目となる本記事では、Web Componentsを支える4つの仕様について解説します。Web Componentsは以下の4つの独立した仕様から構成されます。
- Custom Elements – 独自のカスタム要素をユーザーが定義することを可能にする
- Shadow DOM – Shadow DOMという起点になる要素を提供し、HTMLにスコープを形成する
- Templates – HTMLのテンプレート機能をブラウザネイティブに利用可能にする
- HTML Imports – 断片化したHTMLファイルをロードする
つまり、Web Componentsはそれがひとつの仕様というわけではなく、これらの各機能を組み合わせてHTMLをコンポーネント化する技術のことを指します。よって、各仕様は独立した機能なので、ブラウザが実装していれば単独で利用することも可能です。今回はこれらの仕様について、順に解説していきます。また、記事中で実際のコードを取り扱っていますが、これらを実際に試す場合は、各機能の実装が進んでいるGoogle Chromeで行うことをオススメします。
Custom Elements
Custom Elementsは、ブラウザに新たな要素を定義する仕様です。CSSでのスタイリングやJavaScriptで付与するインタラクションといった特徴をまとめたカスタム要素として登録し、新たなネイティブ要素として利用可能にします。
カスタム要素を定義する
カスタム要素を新しく定義するにはdocument.registerElement
というDOMのAPIを使います。document.registerElement
の第1引数に文字列でカスタム要素名を指定し、第2引数の指定でカスタム要素の挙動が決定されます。ネイティブで定義されているタグとの区別のために、カスタム要素名にはハイフンを含める必要があります。
// <sample-element>を定義する document.registerElement('sample-element');
これでsample-element
という、カスタム要素を使うことができるようになりました。
カスタム要素の挙動を設定する
先程定義したsample-element
は、何の機能も持たない要素です。このsample-element
の挙動をカスタマイズするには、
document.registerElement
の第2引数のprototype
属性に指定します。
// <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype });
このように、HTMLElement
を継承したSampleElementPrototype
オブジェクトを生成し、prototype
属性に指定します。
HTMLElement
を継承することで、HTMLとして基本的な振る舞いをするようになります。挙動を更に細かく制御するために、
SampleElementPrototype
には、ライフサイクルコールバックを指定することが可能です。ライフサイクルコールバックには以下の4つがあります。
createdCallback | 要素が生成されたときに実行されるコールバック関数 |
---|---|
attachedCallback | 要素がHTMLに追加されたときに実行されるコールバック関数 |
detachedCallback | HTMLから要素が除かれたときに実行されるコールバック関数 |
attributeChangedCallback | 属性変更時に実行されるコールバック関数 |
これらのライフサイクルコールバックを必要に応じて利用します。
カスタム要素を利用する
document.registerElement('sample-element')
を実行したあとは、定義したカスタム要素を実際に利用することが可能です。HTML上にsample-element
タグを書くことでも利用可能ですし、以下のようにJavaScriptから生成することもできます。
// <sample-element>を生成する var sampleElement = document.createElement('sample-element');// 生成した<sample-element>をbodyに追加する document.body.appendChild(sampleElement);
また、document.registerElement()
は定義したカスタム要素のコンストラクタ関数を返すので、それをnew
と共に実行することでも生成可能です。
// document.registerElementの返り値を変数に保持する var SampleElement = document.registerElement('sample-element', { prototype: SampleElementPrototype });// <sample-element>を生成する var sampleElement = new SampleElement();
// 生成した<sample-element>をbodyに追加する document.body.appendChild(sampleElement);
既存の要素を拡張する
カスタム要素の作成にはprototype
を利用して挙動をゼロから指定するほか、extends
に拡張したい要素名を指定し、その要素の拡張機能を作成するという方法があります。
// <button>を拡張するextended-buttonを定義する。 document.registerElement('extended-button', { prototype: ExtendedButtonPrototype, extends: 'button' });
extends
を使って作成された要素を利用する場合は、is='extended-button'
のようにextends
で指定した要素のis
属性に指定します。
<button is='extended-button'>This is button</button>
extends
を使って作成された要素は、元々の要素の見た目や内部の特徴を持ちつつも、is='〜'
で指定されたカスタム要素の特徴を持ちます。この場合、button
要素にextended-button
で定義した処理が付与されることになります。
また、extends
属性にはカスタム要素を指定することはできません。prototype
に指定するオブジェクトに継承させましょう。
// <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype });
// これはNG。カスタム要素をextendsすることはできない // 継承したい場合はSampleElementPrototypeを利用する document.registerElement('extended-again-element', { prototype: Object.create(HTMLElement.prototype), extends: 'sample-element' });
Shadow DOM
Shadow DOMはHTMLの世界にスコープの概念をもたらします。HTMLとCSS、そしてJavaScriptを組み合わせて作ったUIコンポーネントの再利用を考えた時に必ず障壁となるのがスコープがないという問題でした。(正確に言えば、iframe
だけはスコープを形成しますが、セキュリティが強く柔軟性に欠け、コンポーネント化という目的は果たせません)
例えばボタンのコンポーネントを作るために.button
というクラスを定義しても、このCSSを別の場所で利用しようとした際に同名のクラスが定義されていると、どちらか一方が上書きされてしまいます。こうした問題に対しては命名規則の工夫等、様々なアプローチがされてきましたが、いずれもカスケーディングを完全に回避できる保証はありません。
これを根本的に解決してくれるのがShadow DOMです。
Shadow DOMの仕組み
Shadow DOMによって、要素は新たにShadow Rootという新たなノードを持つことができるようになります。このShadow Rootを持つ要素はShadow Hostと呼ばれ、スコープの起点となります。Shadow Rootにぶら下がるDOMツリーには外部からアクセスすることができず、内部の処理が外部に漏れることもありません。
つまり、 カスタム要素の振る舞いをShadow DOMに閉じ込めることで、既存のスタイルやJavaScriptに影響されずに扱う ことができます。
実は、Chromeでは既にネイティブのHTMLの要素に、Shadow DOMが使われています。代表的なのがvideo
要素です。video
要素のShadow DOMを確認するには、DevToolsのSettingsの「Show user agent shadow DOM」をチェックする必要があります。
DevToolsでvideo
要素を見てみると、video
の下に #shadow-root があり、その下にdiv
要素やinput
要素がぶら下がっているのが確認できます。div
やinput
にフォーカスしてみると、それらが再生ボタン等のUIコントロール部分を構成しているのがわかると思います。このShadow Hostはvideo
要素ということになります。
video
要素の他にも、textarea
やinput
等でネイティブで使われているShadow DOMを確認することが可能です。
Shadow Rootの生成
では実際にShadow DOMを利用していきます。Shadow Rootを生成するにはcreateShadowRoot()
というDOMのAPIを使います。
createShadowRoot
はHTMLElementをインターフェースとするどの要素からも実行することが可能です。ここでは先程のsample-element
要素内で使います。
// <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);SampleElementPrototype.createdCallback = function () {
// <sample-element>にShadow Rootを生成する var shadowRoot = this.createShadowRoot();
// <style>を生成する var style = document.createElement('style'); var styleString = ''; styleString += 'button {background: #000; color: #fff; font-size: 24px;}'; styleString += 'input {font-size: 24px; background: #cfc;}'; style.innerHTML = styleString;
// <input type='button'>と<button>を生成する var input = document.createElement('input'); input.setAttribute('type', 'text'); var button = document.createElement('button'); button.textContent = 'This is button.';
// 生成した<style>と<input type='button'>と<button>をShadow Rootに追加する shadowRoot.appendChild(style); shadowRoot.appendChild(input); shadowRoot.appendChild(button); };
// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype });
作成したShadow Rootに対し、style
・input
・button
の各要素を追加しました。Shadow Rootに要素が追加されるとShadow Hostの要素は表示されなくなり、代わりにShadow Rootの内容が表示されるようになります。
こちらは実際に動くサンプルです。
style
要素内にはbutton
要素を装飾するCSSを記述していますが、Shadow Root配下にあるので外部に漏れることはありません。また、グローバルな領域にbutton
の装飾をするCSSがありますが、sample-element
内のbutton
に対しては影響していないことが確認できます。
このように、Shadow DOMはHTMLにスコープを提供します。Web Componentsに関連する機能の中でも、最も重要と言える機能かもしれません。
Templates
TemplatesはHTMLをひな形として扱うための仕様です。今までHTMLをひな形として扱いたい場合にはscript
要素を使った方法等がありましたが、これらはあくまでハック的なアイデアに過ぎませんでした。Templatesではtemplate
というタグで括ることで、ブラウザはその中のHTMLを不活性なHTML要素として認識します。不活性な要素は描画されることはなく、document.querySelector()
等でアクセスすることもできません。
HTMLの生成はもちろんDOMのAPIでも可能ですが、HTML生成がJavaScript内で行われていると構造がわかりにくいですし、メンテナンスの観点からもHTML上にテンプレートが配置されているほうが望ましいです。
template要素を利用する
先程の、Shadow Rootに追加しているHTMLをtemplate
を使って書きなおしていきます。Shadow Rootに追加しているHTMLをそのままtemplate
内に記述するだけです。このtemplate
要素にはIDを付与しておきます。
<template id='sample-element-template'> <style> button { background: #000; color: #fff; font-size: 24px; } input { font-size: 24px; background: #cfc; } </style> <input type='text'> <button>Button</button> </template>
先程行っていた、 DOMのAPIでHTMLを生成しShadow Rootに追加する という処理を、 テンプレートをコピーしてShadow Rootに追加する という処理に置き換えます。
// <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);SampleElementPrototype.createdCallback = function () {
// <sample-element>にShadow Rootを生成する var shadowRoot = this.createShadowRoot();
// <template>を取得する var template = document.querySelector('#sample-element-template');
// <template>の中の要素をコピーする var clone = document.importNode(template.content, true);
// 生成した<style>と<input type='button'>と<button>をShadow Rootに追加する shadowRoot.appendChild(clone); };
// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype });
追加したい要素の構造がHTML側に整理されたことで、ぐっと見通しが良くなりました。
HTML Imports
Custom Elements、Shadow DOM、Templatesを使ってsample-element
を作成してきました。ここまでの処理をHTMLファイルにまとめて、そのHTMLをロードすることでsample-element
が利用可能になります。
JavaScriptファイルや画像といったサブリソースをロードするにはscript
やimg
要素を使った方法がありましたが、HTMLに関してはネイティブは存在していませんでした。XMLHttpRequest
を使ってロードする方法もありますが、HTMLのロードに必ずJavaScriptを利用するのもやや大袈裟と言えます。この最も基本的とも言える 外部のHTMLをロードする という機能を実現するのがHTML Importsです。
HTML Importsを利用する
外部のHTMLファイルをロードするには、以下のようにlink
要素を使ってロードします。
<link rel='import' href='sample-element.html'>
断片化されたHTMLファイルはこのようにlink
要素をつかってロードすることが可能で、読み込まれたファイルは読み込み先のHTMLに引き継がれます。ここでは、Custom Elements、Shadow DOM、Templatesを使って構築してきたsample-element
を外部ファイル化します。
<template id='sample-element-template'> <style> button { background: #000; color: #fff; font-size: 24px; } input { font-size: 24px; background: #cfc; } </style> <input type='text'> <button>Button</button> </template><script> // <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);
SampleElementPrototype.createdCallback = function () {
// <sample-element>にShadow Rootを生成する var shadowRoot = this.createShadowRoot(); // <template>を取得する var template = document.querySelector('#sample-element-template'); // <template>の中の要素をコピーする var clone = document.importNode(template.content, true); // 生成した<style>と<input type='button'>と<button>をShadow Rootに追加する shadowRoot.appendChild(clone);
};
// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype }); </script>
テンプレートとなるHTMLから、実際にsample-element
を定義するスクリプト処理までをまとめてsample-element.html
としました。
このように単一のHTMLファイルに集約することでコンポーネントの責任の在処も明確にすることが可能です。
外部ファイル化したsample-element.html
をロードする最も単純な例は以下のようになるでしょう。以下をindex.html
とします。
<html> <head> <link rel="import" href="sample-element.html"> </head> <body> <sample-element></sample-element> </body> </html>
しかし、このsample-element.html
をいざインポートしようとすると、エラーが発生します。具体的にはdocument.querySelector('#sample-element-template');
で要素が存在しないためにnull
を返すためです。
インポート時のdocumentの扱いに注意する
HTMLの評価はindex.html
側で行われるため、querySelector
の実行者であるdocument
はロード先のindex.html
になります。そうするとsample-element.html
に配置してある#sample-element-template
はindex.html
にないので、要素が見つからないという結果になってしまいます。
そのため、querySelector
の実行者をsample-element.html
のdocument
にする必要がありますが、これにはdocument.currentScript
という属性を利用します。document.currentScript
では実行中のスクリプトノードを返します。ノードからはownerDocument
を使って親となるドキュメントを参照することができるので、これらを組み合わせてquerySelector
の実行者がsample-element.html
のドキュメントになるようにします。
<script> // 実行中のスクリプトを参照する var currentScript = document.currentScript;// <sample-element>のプロトタイプ var SampleElementPrototype = Object.create(HTMLElement.prototype);
SampleElementPrototype.createdCallback = function () {
// <sample-element>にShadow Rootを生成する var shadowRoot = this.createShadowRoot(); // <template>を取得する var template = currentScript.ownerDocument.querySelector('#sample-element-template'); // <template>の中の要素をコピーする var clone = document.importNode(template.content, true); // 生成した<style>と<input type='button'>と<button>をShadow Rootに追加する shadowRoot.appendChild(clone);
};
// <sample-element>を定義する document.registerElement('sample-element', { prototype: SampleElementPrototype }); </script>
querySelector
の実行者がsample-element.html
のdocument
になったことで正常に動くようになります。その直後のdocument.importNode
やdocument.registerElement
はそのままにしてあることにも注目してください。ノードのコピーや、カスタム要素の登録はインポート先のドキュメントで行うのが適切と言えるでしょう。これで晴れてsample-element.html
のインポートが正常にできるようになりました。
まとめ
Custom Elements、Shadow DOM、Templates、HTML Importsの4つの仕様の基本的な使い方について解説しました。簡単におさらいすると、以下のようになります。
- Custom Elementsでカスタム要素を新たに定義し、基本的な挙動を指定する。
- カスタム要素に指定するCSSやJavaScriptの効力はShadow DOMに閉じ込める。
- テンプレートとして扱うHTMLを
template
タグに宣言する。 - カスタム要素を定義する一連の処理を記述したHTMLを、HTML Importsで読み込む。
再利用可能なコンポーネント化を実現するために、それぞれがどういった役割を果たしているかを理解することはもちろん重要ですが、それらはあくまで独立した仕様であり、単一の機能として利用できることも認識しておきましょう。
次回は、より実践的なコンポーネント作成を解説する予定です。