WEBマーケティング事業部 エンジニアのDuenoです。
今回はWebComponentsの話です。よろしくおねがいします。
WebComponentsとは
Webコンテンツに含まれる様々なパーツを、コンポーネントとして定義し、再利用可能な形で公開する為のものです。
例えば、Webブラウザ上で動画を再生するのに用いられる<video>タグは、
GoogleChromeではWebComponentとして実装されています。
定義されたWebComponentは、そのタグを記述するだけで利用できます。
コンポーネントの内部はShadow DOMという技術によって隠蔽されており、
Webページとコンポーネントが不本意に干渉し合う事を防いでいます。
WebComponents実用例
マークダウンテキストのレンダリングタグ
https://github.com/robdodson/mark-down/
<mark-down> # テストA ## テストB </mark-down>
と記述すると、<mark-down>タグ内のテキストがマークダウンテキストとして処理され、

のように描画されます。
画像の遅延読み込み
WebComponentsは、既存のタグを拡張することも出来ます。
https://github.com/1000ch/lazyload-image/
このコンポーネントは、対象のimgタグ要素が画面に表示された時に初めて画像のロードを行うというものです。
<img is="lazyload-image" src="image.jpg" >
のように is='~~' という記述によってデフォルトのimgタグと使い分けが可能です。
WebComponentsの4要素
WebComponentsはShadowDOMを含む以下4つのWeb標準で構成されています。
- Custom Elements
- Shadow DOM
- HTML Templates
- HTML Imports
1. Custom Elements
Custom Elementsは、HTMLElementそのものを拡張し、独自の要素を定義する仕組みのことです。
var XComponent = Object.create(HTMLElement.prototype);
document.registerElement('x-component', {
prototype: XComponent
});
↓
<x-component></x-component>
独自に定義したタグに対して、機能を持たせることが出来ます。
ちなみに、ES6のclass構文を使ってHTMLElementをextendする形でCustomElementが定義できるようになるかも。 https://w3c.github.io/webcomponents/spec/custom/
2.Shadow DOM
従来のWebページは単一のDOM Tree(DocumentTree)で構成されており、全てのHTMLElementがDocumentTreeに属していた(存在していた)為、 抽象化というが概念が存在しませんでした。 Shadow DOMはDocumentTreeと干渉することのないDOM Tree(ShadowTree)を用いて、DOMのカプセル化を実現したものです。

例えば、
var root = htmlElement.createShadowRoot(); root.innerHTML'<button id='shadow-button'>送信<button>';
と記述するとhtmlElement下にShadowRootが生成され、ShadowRoot下に送信ボタンが生成されます。 送信ボタンはShadowTreeに属している為、
document.getElementById('shadow-button')
のように選択することが出来ません。
3.HTML Templates
htmlテンプレートを定義する為のHTMLElement(タグ)の事です。 JavaScriptから、テンプレートHTMLをコピーしてページ要素を生成したい場合、 従来のやり方ですと、
<div stype='display:none;'> //テンプレートを記述 </div>
であったり
<script type="text/template"> //テンプレートを記述 </script
というように実装していたところを、templateタグを利用して
<template id="sample-template">
<style>
...
</style>
<div id="container">
<img src="http://webcomponents.org/img/logo.svg">
</div>
</template>
と記述する事が出来るようになります。 templateタグ内の要素は自律動作できないものとして扱われ、 スクリプトは実行されず、画像のロード等も行われる事はありません。 ドキュメントにも存在しないものとして扱われ、タグ内の要素を複製する事で初めて有効になります。
var template = document.querySelector('#sample-template');
t.content.querySelector('img').src = 'image.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
4.HTML Imports
linkタグを用いて、JavaScriptやCSS等のリソースをhtmlとしてまとめて記載し、インポートすることが出来るようになります。
<link rel="import" href="component.html"> <title>Import Example</title> <script src="script3.js"></script>
<script src="js/script1.js"></script> <script src="js/script2.js"></script>
と記述すると、<link rel="import" href="component.html">の行でcomponent.htmlが展開され、index.htmlではscript1.js,script2.js,script3.jsがロードされます。
この時、component.html内の要素はDocumentTree内に展開される為、
document.querySelector('script')
等の記述で選択する事が出来ます。
実際にコンポーネントを作る
例えば、ブログの記事ページにつけるTwitterのShareボタンや、 独自実装の動画プレイヤーを外部サイトに埋め込む場合、 埋め込み対象サイトのコンテンツと干渉しあわないようにiframeタグを使った実装が よく行われています。
この記事では外部サイトに埋め込む想定のパーツを、WebComponentsを用いて実装しました。
サンプル
- 開始月、終了月を指定するとその範囲のデータが表示されるコンポーネント
monthly-reportを作る


1. 指定した期間の月毎のデータをテーブル形式で表示させるコンポーネント <monthly-table>を作る
仕様
<monthly-table startmonth='2' endmonth='5'></monthly-table>
と記述しておくと

のようなテーブルが表示されます。 'draw'イベントで再描画が可能とします。 データは、今回コンポーネント内に直書きするものとします。
<!DOCTYPE html>
<template id="base-table">
<table id="monthly-table">
<thead>
<tr>
<th>月</th>
<th>収穫量</th>
<th>売上</th>
</tr>
</thead>
<tbody id="monthly-table-body"></tbody>
</table>
</template>
<script>
var monthlyTableDocument = this.document.currentScript.ownerDocument;
var MonthlyTable = Object.create(HTMLElement.prototype);
var data = [
[21,42],
[22,44],
[23,46],
[24,48],
[25,50],
[26,52],
[27,54],
[28,56],
[29,58],
[30,60],
[31,62],
[32,64]
];
MonthlyTable.createdCallback = function () {
console.log(window)
var self = this;
var root = this.createShadowRoot();
var template = monthlyTableDocument.querySelector('#base-table');
var clone = document.importNode(template.content, true);
root.appendChild(clone)
self.addEventListener('draw', function() {
var startmonth = +this.getAttribute('startmonth');
var endmonth = +this.getAttribute('endmonth');
var tbody = root.getElementById('monthly-table-body');
tbody.innerHTML='';
for(var i=startmonth;i<=endmonth;i++){
tbody.innerHTML+=('<tr><td>'+i+'</td><td>'+data[i][0]+'</td><td>'+data[i][1]+'</td></tr>');
}
});
var drawEvent = new Event('draw');
self.dispatchEvent(drawEvent);
};
document.registerElement('monthly-table', {
prototype: MonthlyTable
});
</script>
今回は例としてのわかりやすさを重視してシンプルなものにしましたが、グラフの描画なども実装可能です。 ちなみに、GoogleChartsをWebComponentsとして実装したものがありました。 https://github.com/GoogleWebComponents/google-chart google-chartを使うと
<google-chart data='sample.csv'><google-chart>
の記述だけでグラフを表示させる事ができます。

2. <monthly-table>に期間を投げるための<daterange-picker>を作る
仕様
daterange-pickerはテキストボックスを2つ持ち、それぞれ開始月と終了月を入力することが出来ます。 入力をすると、
<daterange-picker startmonth='1' endmonth='2'></daterange-picker>
というようにカスタム要素の属性が更新されます。 pickerと言いながら今回は数値を入力するだけのものになります。
<!DOCTYPE html>
<script>
var DaterangePicker = Object.create(HTMLElement.prototype);
DaterangePicker.createdCallback = function () {
var self = this;
var root = this.createShadowRoot();
root.innerHTML = '<input type="text" id="startmonth"><input type="text" id="endmonth">';
var update = function(){
self.setAttribute(this.id,this.value);
}
root.getElementById('startmonth').onchange = update;
root.getElementById('endmonth').onchange = update;
};
document.registerElement('daterange-picker', {
prototype: DaterangePicker
});
</script>
3. <monthly-table>と<daterange-picker>が連携し、shadowDOMで隠蔽された<monthly-report>を作る
<monthly-report>内に <monthly-table>、<daterange-picker>に加え更新ボタンを設置します。
<!DOCTYPE html>
<link rel="import" href="monthly-table.html">
<link rel="import" href="daterange-picker.html">
<template id="report-template">
<daterange-picker></daterange-picker>
<button id="update-button">更新</button>
<monthly-table startmonth="2" endmonth="5"></monthly-table>
</template>
<script>
var monthlyReportDocument = this.document.currentScript.ownerDocument;
var MonthlyReport = Object.create(HTMLElement.prototype);
MonthlyReport.createdCallback = function () {
var self = this;
var root = this.createShadowRoot();
var template = monthlyReportDocument.querySelector('#report-template');
var clone = document.importNode(template.content, true);
root.appendChild(clone);
var daterangePicker = root.querySelector('daterange-picker');
var monthlyTable = root.querySelector('monthly-table');
root.getElementById('update-button').onclick = function(){
monthlyTable.setAttribute('startmonth',daterangePicker.getAttribute('startmonth'));
monthlyTable.setAttribute('endmonth',daterangePicker.getAttribute('endmonth'));
var drawEvent = new Event('draw');
monthlyTable.dispatchEvent(drawEvent);
};
};
document.registerElement('monthly-report', {
prototype: MonthlyReport
});
</script>
これで完成です。あとは、
<link rel="import" href="monthly-report.html"> <monthly-report></monthly-report>
と記述すると埋め込まれるはずです。 完成したデモ
iframeと比較した際のWebComponentsの長所・短所
長所
埋め込みコンテンツを操作できる。
iframe要素のソースが別ドメインだった場合、クロスドメイン制約に抵触するため、iframe要素内のコンテンツを操作することは出来ません。 しかしWebComponentsを用いた実装の場合、カスタム要素よりも下層はShadow DOMによって隠蔽されているが、 埋め込んだカスタム要素そのものはDocumentTree内の要素としてフルアクセス可能なので、ここに予めEventを定義しておく事で、限定的に要素内コンテンツを操作することが可能です。

短所
非対応ブラウザが多い
WebComponentsは現在も標準化に向けて協議が行われています。 そのため、対応ブラウザも決して多くはありません。 しかし、Polyfillというものがあり、
<script src="~~/webcomponents.js"></script>
と記述しておくことで非対応ブラウザでもWebComponentを動作させることが出来ます。
学習・実装コストが高い
iframeと比べると、実装の難度が高く、コードの複雑性が高まる事が考えられますが、 Polymerを使ってWebComponentを実装することで、 比較的少ないコード量で記述することが出来ます。
WebComponentsの対応状況 - http://webcomponents.org/

参考文献
W3C Web Components Specifications - https://w3c.github.io/webcomponents/
HTML5 ROCKS Shadow DOM - http://www.html5rocks.com/ja/tutorials/webcomponents/shadowdom/
WebComponents.org - http://webcomponents.org/