Web Components 入門

このエントリーをはてなブックマークに追加

はじめに

Advent Calendarによって書かれた記事である。

第2のドワンゴ Advent Calendar 2015の9日目を担当するabout_hiroppyです。
今回はWeb Componentsについてできるだけ簡潔にまとめたいと思います。

SHADOW DOMではサンプルを描画しているのでchrome推奨です。

Web Components とは?


WebComponents.org

将来、ウェブの標準になる予定の機能です。
UIの再利用性を高めるためにコンポーネント化を目的とした機能のまとまりで、
それぞれの機能自体も優秀ですが、組み合わせることにより更に利便性が上がります。
Web Componentsの現状 の記事の冒頭で今の現状が書かれています。

機能

以下の機能から構成されます。

  • CUSTOM ELEMENTS
  • HTML IMPORTS
  • TEMPLATES
  • SHADOW DOM

CUSTOM ELEMENTS

ネイティブで実装されていない独自のHTML要素を定義します。
また、既に定義されているHTML要素の拡張も行えます。
定義されたHTML要素はJavaScript等で既存のタグと同等に使うことが可能です。
document.registerElement()で登録することにより、HTMLUnknownElement InterfaceではなくHTMLElement Interfaceを継承します。

document.registerElement()

document.registerElement(tag-name, ElementRegistrationOptions)

const xElement = document.registerElement('x-element', {  
  prototype: Object.create(HTMLElement.prototype)
});
tag-name

登録するタグ名を渡します。

注意点として以下があります。

  • タイプ名は必ず - でつなげる必要があります
    • e.g. <x-element>, <my-element>
  • 大文字は使用できません
ElementRegistrationOptions(option)

カスタム要素プロトタイプを渡します。
HTMLElementではインスタンス化の機構をもたないのでObject.create()を使いオブジェクトを生成します。
ライフサイクルコールバックを設置することにより、以下のイベントを登録することが可能です。

  • createdCallback
    • 生成された時に呼び出される
  • attachedCallback
    • ノードリストに登録されると呼び出される
  • detachedCallback
    • ノードリストから消されると呼び出される
  • attributeChangedCallback
    • 属性が変更されると呼び出される

既存の要素を拡張する場合

ElementRegistrationOptionsにextendsプロパティを追加します。

以下ではimgを拡張しています。

const xCustomImg = document.registerElement('x-custom-img', {  
  extends  : 'img',
  prototype: Object.create(HTMLImageElement.prototype)
});

is属性を使い関連付けます。

<img is="x-custom-img">  

HTML IMPORTS

linkタグからHTMLをロードします。
html importsによりiframeやXMLHttpRequestを使わなくても読み込むことが可能になりました。

<!-- index.html -->  
<link rel="import" href="./imports/article.html">  
<!-- ./imports/article.html -->  
<link rel="stylesheet" href="article.css">  
<script src="article.js"></script>

<div id="article"></div>  

article.html内でJavaScriptやCSSを読み込むことによりそのページで使用するものをまとめることが可能となります。

import先のコンテンツにアクセスする

const link    = document.querySelector('link[rel="import"]');  
const content = link.import.querySelector('#article');  
document.body.appendChild(content.cloneNode(true));  

import先のコンテンツから部分的に引っ張りクローンすることが可能です。

importされたJavaScriptの扱いについて

  • 呼び出し元(今回だとindex.html)のwindowに接続されます
  • メインページのパースをブロックしません

TEMPLATES

template タグを使います。
雛形を作り、HTML上にDOMベースで利用します。
ドキュメント上に存在しない扱いなので、どこにおいても構わないです。
使用するまでは中身に書いてあるコードは評価されません。
IEの場合、対応していないためそのまま中身が表示されてしまうのでpolyfillが必要です。

<template id="mytemplate">  
  <h1>Template!</h1>

  <!-- 利用されるまで以下の画像等は評価されない -->
  <img src="template.png">
  <script>alert('template!')</script>
</template>

<div class="wrapper">  
  <div class="container">
    <h1>😋</h1>
  </div>
</div>  

利用されるまで副作用を持ちません。

アクティベートにする

const template = document.querySelector('#mytemplate');  
const clone    = document.importNode(template.content, true);  
document.body.appendChild(clone);  

レンダリングされたことによりtemplateの中身が評価されます。

SHADOW DOM

ShadowRootという他のDOMからの影響がないノードを生成し、入れることにより、scopeの解決を行います。
これにより、スタイルを外部からカプセル化でき、cssのスタイル指定がより柔軟に簡略化できます。

ShadowRoot

DocumentFragment interfaceを継承したものです。
どの要素にもShadowRootオブジェクトを作成できます。
作成したらその要素はShadow Hostとなります。 複数作成することは可能ですが、LIFOなので最後に追加したShadowRootのみがブラウザに表示されます。

ShadowRootを作成する

Element.createShadowRoot() を用いて作成します。

<span>hello world!</span>  
const span = document.querySelector('span');  
span.createShadowRoot().textContent = 'Shadow DOM!';

// output
// <span>Shadow DOM!</span>

結果 (Shadow DOMが有効ならば以下には Shadow DOM! と表示されているはずです)

hello world!

要素を確認すると以下のようになります。
ShadowRootの内容が優先的に表示されます。

複数作成した場合

🔔 現在、Multiple Shadow RootsはDeprecatedになっていますので注意してください

先ほど述べたように最後のものが反映されます。

const span  = document.querySelector('span');  
const root1 = span.createShadowRoot();  
const root2 = span.createShadowRoot();

root1.innerHTML = '<span>root1</span>';  
root2.innerHTML = '<span>root2</span>';

// output
// <span>root2</span>

結果

hello world!

となり、spanはroot2という文字列に置き換わります。

SHADOW要素を用いることにより一つ前のShadowRootを表示することが可能です。

root1.innerHTML = '<shadow><span>root1</span></shadow>';  
root2.innerHTML = '<shadow><span>root2</span></shadow>';

// output
// <span>hallo world!</span>

hello world!

root1.innerHTML = '<span>root1</span>';  
root2.innerHTML = '<shadow><span>root2</span></shadow>';

// output
// <span>root1</span>

hello world!

状態を確認する
// 最新のshadowRootを取得する
console.log(span.shadowRoot);

// 一つ前のshadowRootを取得する
console.log(root1.olderShadowRoot);  
console.log(root2.olderShadowRoot);  

root1より前のshadowRootは存在しないのでnullとなります。

Shadow Tree

innerHTML等で拡張していくとShadow Treeというツリーができます。
Shadow Treeは以下のように構築されます。

  • JavaScriptにより動的に生成する
  • templateタグを用いることにより静的に生成する

CSSを適応する

::shadow擬似要素を用いてアクセスします。

<span class="eg-shadow-dom eg-shadow-dom-color">hello world!</span>  
eg-shadow-dom-color {  
  color: #aa0;
}

/* Shadow Tree以下の直下セレクタを指定 */
eg-shadow-dom-color::shadow > span {  
  color: #3498DB;
}
const span = document.querySelector('.eg-shadow-dom');  
span.createShadowRoot().innerHTML = '<span>font color</span>';

// output
// <span style="color: #3498DB;">font color</span>

結果

hello world!

有効であれば上記の文字は水色になったはずです。
.eg-shadow-dom-color::shadow のように指定してスタイルのカプセル化をすることが可能です。

ブラウザサポート状況

Can I use web components?
2015/12/09現在

  • CUSTOM ELEMENTS
    • Chromeのみサポート
  • HTML IMPORTS
    • Chromeのみサポート
  • TEMPLATES
    • IE以外はサポート
  • SHADOW DOM
    • Chromeのみサポート

もしWeb Componentsをつかうのであれば、polyfillのライブラリは必須な感じです。

// 確認方法

// Custom Elements
!!document.registerElement;

// HTML Imports
document.createElement("link").import === null;

// Templates
!!window.HTMLTemplateElement;

// Shadow DOM
!!Element.prototype.createShadowRoot;

ライブラリ

まとめ

個人的にははやく標準で入ってほしい機能ばかりですがあと何年後になるのかなぁって感じもします。
当分はPolymerを使ってかなと思います。

本当はtemplateを使い、Shadow DOMを生成してCustom Elementsを作成して拡張してimportするのをやりたかったのですが、
記事が長くなりすぎると判断したので次でまとめれればまとめたいと思います。

参考

http://www.w3.org/TR/shadow-dom/
http://www.w3.org/TR/custom-elements/
http://www.html5rocks.com/ja/tutorials/webcomponents/shadowdom-201/
http://www.html5rocks.com/ja/tutorials/webcomponents/template/
http://www.h2.dion.ne.jp/~defghi/webc/webc.htm