Web Componentsの基本的な使い方・まとめ

Web Componentsとは

Web ComponentsとはW3Cで現在仕様を検討しているHTML環境に対するJavaScript向け拡張キットであり, WEBページに対するアドオン・プラグイン機構を定義します. これまでもスクリプトファイルやCSSシステム等のように, ファイル単位でのライブラリと言ったものはありましたが, Web Componentsはこれらを集約することが出来るなど, 従来の仕組みを更に洗練したものとなっています. また, 既存のページに対する変更を最小限にしながら外側から機能・構造を強化(挿入)できるため, アクセシビリティを確保しつつ高度なアプリケーション機能, WEB機能のコンポーネント化を実現できます.

Web Componentsの背景

かつてのWEBブラウザといえばHTML文書の表示に特化したアプリケーションでしたが, コンピュータの進化に伴い様々な面で機能が強化されていきました. その結果, 従来では考えられないほどに多彩なサービスやWEBアプリケーションが次々に生み出されています. これはつまりWEBブラウザのアプリケーションプラットフォーム化が進んだということであり, HTMLで記述されたアプリケーションをWEBブラウザで動作させているのです.

その一方で本来の意味におけるHTMLは, 単に文書を伝達するための手段に過ぎません. 従って, より高度な要件が求められるアプリケーションをマークアップするにはいささか機能が不足しており, 様々な面で無理が生じています. 例えば一般的なアプリケーションでは様々な部品を組み合わせて最終的な成果物を構成していきますが, HTMLではこの部品化や再利用と言った面での機能に乏しく, 開発作業の度に同じような作業を繰り替えすと言った事はよくみられます.

さて, この問題を解決するにはどうしたらよいのでしょうか. 例えばJavaScriptを用いた専用のフレームワークを導入したとしましょう. しかし, 外部的なフレームワークを用いた対処ではその仕様にアプリケーション側が縛られてしまいます. また実現したいことは広く一般的な内容なのですから, いつ使えなくなるか判らないフレームワークに頼るより, 標準化された手段を採用したほうが長期にわたるノウハウの蓄積やリソースの共有化もやりやすいというものです.

こうして生まれたものがWeb Componentsです. Web Componentsは既存のWEBブラウザの内部で動作し, HTML環境の部品化を強力にサポートします. 部品の生成はこれまでどおりHTMLとCSS及びJavaScriptを使って行うため, WEBアプリケーション構築で威力を発揮するでしょう. また, 既存のDOM仕様の拡張となっていることから, 手に馴染んだスクリプトライブラリを手放すこと無く新しい技術の恩恵を受けられるというメリットもあります.

Web Componentsの構成

Web Componentsは主に次の4つの分野から構成されています. いずれも個別の仕様として定義されており単独で利用することも可能ですが, 組み合わせて利用したり他の仕様と連携させることでより効果的に使えるように設計されています.

  • template要素(HTML Templates)
    HTMLDOMをスクリプト等から操作する際に必要となるツリー構造のひな形を定義しておく仕組みを提供します.
    SVG要素でのdefs要素に近い機能で, HTML5仕様として正式に取り入れられました.
  • HTML Imports
    外部のHTML文書の内容をメインとなる文書に取り込みます. 複数の異なるリソースを一つにまとめる効果を持ちます. また, リソース参照の連鎖と言った新たな概念を導入します.
    XMLHttpRequestを使っても実現できますが, より簡潔に記述できるようになります.
  • Shadow DOM
    既存のDOMツリーの内容をデータ(モデル)とビュー(レイアウト)に分割し, スクリーンに描画される内容を上書きします. 文書構造ツリーをより柔軟に構成することが可能になります.
    canvas要素のインタラクティブフォールバック, SVGのuse要素の発展系と考えられます.
  • Custom Elements
    HTMLDOMそのものを拡張し, 独自の要素を定義します. また, 既存の要素を拡張することもできます. このことからHTMLDOMの継承関係ツリーを拡張するための仕様と言えます.
    一般に実現方法が多数存在するElementオブジェクトの拡張に対して, 統一された手段が提供されます.

これらの機能は従来のHTMLのAPIを利用するだけでも再現できましたが, 標準仕様としてその手段が提供されることで, コードの再利用性が高まります.

補足)宣言的なコンポーネント定義

Web Componentは当初スクリプトを介した手続き的な部品の登録のみならず, HTML上のマークアップによる宣言的なコンポーネント定義をも視野に入れていました. しかし, 後者の仕様化は様々な課題を抱えていることから初期段階での仕様検討が見送られることになりました. 今後, Web Componentsが技術的な成熟を見たタイミングで再度仕様が見直されるかも知れません.

Web Componentsとアクセシビリティ

Web Componentsはその仕組みから高機能性が問われるWEBアプリケーション分野での活用を想定しているように見えますが, それだけではありません. 一般にアクセシビリティを意識したWEB設計を行った場合, HTMLの特性から利用できる機能に制限が掛かることはよくあります. しかし, Web Componentsを導入することでアクセシビリティを確保した上で環境に応じたリッチな機能拡張を行うことが可能となるのです.

なお, Web Componentsそのものにはアクセシビリティに関わる仕様が存在しません. 従って, アクセシビリティを確保するのであれば, WAI-ARIAと組み合わせるなどの何らかの作法に則ったWEB部品定義が求められます.

Web Componentsとセキュリティ

Web ComponentsはWEBを取り巻く技術の中でも新しいものです. 従って, その使い方についてのノウハウが全く蓄積されていませんから, 誤った使い方をすることでコンポーネントの再利用性や汎用性を損なったり, 未知のセキュリティホールを開けてしまうことは十分に考えられます. 従って使う側もそのリスクについてはよく理解した上で導入すべきです.

Web Components導入によるデメリット

Web Componentsの導入がもたらすことは良いものばかりではありません. Web Componentsは既存のWEBがはらむ構造上の複雑さを隠蔽することで, 見た目上シンプルにするとされています. しかし全体としての複雑度は, むしろWeb Components仕様が追加されることで増加する点を忘れてはなりません. 不具合の原因が様々なパーツに分散・隠蔽されることで, かえって原因解明を困難にすることもあるのです. 例えば次のような点です.

  • リソース管理が複雑化します
    これまではモジュールに分解したとしても比較的フラットな構造をとっていたHTMLですが, Web Componentsを導入することでリソース参照のツリー化が発生し, ファイル変更が与える影響の範囲を特定することが難しくなります.
  • ネーミングルールの範囲が拡大します
    Web ComponentsはHTMLそのものを拡張するため, スクリプト変数名と同様に何らかのルールを敷かないと名称の衝突や機能の重複といった問題を引き起こします.
  • 構造が分散管理されることで全体を俯瞰することが難しくなります
    単一の巨大な構造が管理しにくいのは事実ですが, 機械的に処理しやすいというメリットもあります. これを細かく分割すると一つ一つの構造は把握しやすくなりますが, 適切に分割されていなかった場合, 全体としてどのような形となるかをイメージすること難しくなります.

つまり既存の構造がよく整理されており, その役割分担が明確に管理されているならば, これまでとは異なる切り口でより効率的にリソースの管理が可能となるだけなのです.

また, この他にも考慮すべき点はあります.

  • 原則的にJavaScriptが動作する環境でのみ有効です
    Web Componentsでは全ての機能がJavaScriptから操作されることを想定しています. そのため, JavaScriptが動作しない環境との機能ギャップが更に拡大します.
  • 習得にコストがかかります
    Web Componentsを構成する4分野はそれぞれ個別に用いることも出来るなど, 決して難しいものではありません. しかし全体としての役割分担をイメージ出来るようになるためにはそれなりに時間が掛かります. また現状ではWeb Componentsをサポートする開発環境に乏しく, デバッグやプログラミング上のノウハウが不足しています.
  • コンポーネントの管理が重要となります
    複数のWEB部品を組み合わせる場合, コンポーネント間の依存性のみならず, コンポーネントのバージョン, 内部で利用しているJavaScriptライブラリ間の相互運用性等も管理対象となります. これはかつてのDLL地獄のWEB環境における再現であり, システムが成長・肥大するにつれライブラリの管理の重要性が増していきます.

このように, 新たな開発・管理の環境・体制を整える必要が出てきます. いかなWeb Componentsといえども, 基本的にはHTMLDOMの扱いに対する拡張に過ぎず, これまで行っていたことが, 今後多少やりやすくなるというだけですから過度な期待は禁物です. 場合によっては敢えて使わないと言った勇気も必要でしょう.

ではWeb Componentsを学ぶ必要はないか?

いいえ, そんなことはありません. Web Components仕様は机上で決定されたものではなく, 事実上の動作環境と言えるChromeでの実装を元に様々な開発者からのフィードバックを得た言わば生きた仕様であり, その有効性については既に確かめられています. そのため, 今後は好むと好まざるに関わらずWeb Componentsに触れる機会は増えていく事でしょう. 従って, 現状細かい部分はともかく, 全体としてどのような構成となっているのか程度は理解しておくべきです.

Web Componentsと競合する技術

Web Componentsの他にも既に似たような仕組みをもつ技術は存在します. 既に理解しているものがあれば, それらと比較することでより深くWeb Componentsを理解できることでしょう. またWeb Componentsと組み合わせて利用することが可能なものもあります.

既存のHTML機構との相似

HTML仕様における相似点をみてみましょう.

frame/frameset/iframe/object/embed要素

frame/frameset要素は複数のHTML文書を見た目上一つにまとめて表示するものです. iframe/object/embed要素は他の文書を任意の部分に埋め込むための仕組みです. 何れも各文書はそれぞれ独立しているため, ドキュメントレベルでの部品化と考えられます. 一方のWeb Componentsではより細かいノードレベルでの部品化が可能です.

また, フレーム内に個別のツリー構造を展開する点でShadow DOMと非常によく似ています. 個々のツリーがiframeではwindowを介して分離されているのに対し, Shadow DOMでは親子関係で強く結び付いている点が異なります.

scoped属性をもつstyle要素

スタイルの有効範囲を限定することから, Shadow DOMによるスタイルのカプセル化に近い仕組みと言えます. 現在scoped属性付きのstyle要素が有効な環境はFireFoxのみと, 限られています.

canvas要素

canvas要素は内包するコンポーネントの描画処理をスクリプトでオーバーライドするという観点からShadow DOMに似た機構と考えることができます. Shadow DOMでは既にあるHTMLDOMを使ってレンダリングを行うのに対し, canvas要素では全てをスクリプト側で制御します.

SVGのdefs要素/use要素

SVGのdefs要素はグラフィック構造の定義を行うもので, それ自身は描画されません. 従ってtemplate要素に近い役割を持ちます. use要素は図形の参照・複製を行う点と, それ自身は(図形要素としての)子を持たないことからtemplate要素の参照機能とShadow DOMの仕組みを内包している事になります. defs要素の内容は自動的にDOM上に展開され, JavaScriptによる制御を要せずに動作できる点がtemplate要素と異なる点です.

なお, SVG2においてはuse要素の挙動がShadow DOMの観点から見直される可能性があります.

CSS

HTMLに対するスタイル付け言語であるCSSはWeb Componentsと組み合わせて利用できますが, HTML文書の構造を維持しつつ見た目の構造を変更することが出来る点で共通しています. 特に次に示す二つの機能は見た目や仕組みがShadow DOMによく似ています.

::before, ::after擬似要素

HTML要素においては::before, ::after擬似要素を用いることで見た目専用の構造を挿入することが出来ます.

element()関数による背景の挿入

CSS4で定義されるelement()関数を用いると, 既存のDOMの内容を背景画像とすることが可能です.

XSLT

WEBの部品化の観点からはXSLTが挙げられます. Web Componentsとは次の点で異なります.

  • XSLTはXMLに対する技術であり, 厳密にはHTMLを対象としていません.
  • XSLTにはXMLによる名前空間の仕組みが定まっており, 明確にコンポーネントを分離することが可能です. Web Componentsではこの点がまだ不明瞭です.
  • XSLTはXMLに対する静的なビューを与えます. Web ComponentsはHTMLに対する動的な(ユーザー応答性のある)ビューを定義します.
  • XSLTはXML構造を変化させるだけなので, 実現可能な機能が文書変換の範囲に留まるため, Web Componentsほどの自由度を持ちません.

かつてXSLTもWEB上での様々な問題を解決するものとして作られました. しかし未だフロントエンド用途で普及したとの話を聞きません. しかしWeb Componentsとは異なるアプローチを採っていることから, 今後上手い連携の方法が編み出されるかも知れません.

Flash/Javaアプレット

Custom Elementsで作成したコンポーネントは, 内部に独立した構造をもち, 外部からは専用のAPIを使ってアクセスするという点でFlashやJavaアプレットによるコンポーネントによく似ています. しかし, 仕組み上内包するコンテンツに容易にアクセスすることが可能であり, (専門的知識を必要としない点で)カジュアルコピーのリスクを軽減する効果は全くありません.

ECMAScript6 template strings

JavaScriptの次期標準仕様ECMAScript6では, これまで出来なかった複数行にまたがる文字列リテラルの記述が可能となります. つまりスクリプト中にHTML構造を文字列として挿入できます. HTMLではinnerHTMLプロパティを介してHTMLソースとDOM構造とを相互に変換することが出来るため, template要素を使った記述と完全に競合します. なお, template要素の初期案では単なるJavaScript用のツリー構造置き場に留まらない, より広範な目的で利用できるものとされていましたが, 実現に至る過程で計らずも似たような内容となってしまいました.

element behavior/HTML Components(HTC)

element behavior/HTML ComponentsはMicrosoftがかつてInternet Explorer向けに提供していたHTMLの拡張機構で, Web ComponentsにおけるHTML ImportsやCustom Elements等に加えCSSの拡張やカスタムイベントと言った機能を提供していました. 現在ではWEB標準化の観点から極限られた環境でのみ動作可能です.

Web Componentsを使う

Web Componentsを利用する場合, それをとりまく環境について知っておく必要があります.

Web Componentsの導入対象

Web Componentsは基本(X)HTML文書において利用されることを想定しています.

補足)SVG文書とWeb Components

WEBブラウザにおいては, SVGファイルは単体でもSVG文書として扱うことができます. しかしSVG文書においては現状(DOMAPI仕様上は使えるように見えるものの)Web Components機構のほとんどを利用することが出来ません. しかし次期SVG2仕様ではHTMLとの境界線が曖昧となることから, SVGに対するユースケースも検討対象となるかもしれません.

Web Componentsの動作環境

Web Componentsを動作させるには少なくともHTML5をサポートしているブラウザが必要です. なおWeb Componentsは未だ仕様が確定しておらず, HTML仕様について横断的に影響を及ぼすものが多いため, 現状利用できる環境が非常に限られています. 以下は2014年12月現在での主要なWEBブラウザにおけるWeb Componentsのサポート状況です.

この内Chromeでの実装が最も進んでおり, 開発ツール上での配慮もあります. とは言え, キーボードでの操作が考慮から漏れているなど, 導入するには対象OSや利用環境の制限, 仕様不備に依る不具合の許容等の割り切りが必要です.

FireFox環境での設定

FireFoxでは現状Web Componentsの実装は一部に留まっており, 全ての機能を利用するにはフラグ設定が必要です. そのため, 動作検証などの用途にしか使えません. なお, 正式にWeb Componentsのサポートが宣言されたため, 近い将来ほぼ全ての機能が利用可能となるでしょう.

(1)URLバーに「about:config」と入力し, 詳細設定画面を開きます.
(2)フラグ「dom.webcomponents.enabled」をの値を「true」に変更します. 

それ以外の環境

Internet ExplorerやSafariにおいても仕様の調査に入っており, Web Componentsの未来は明るいとの見解もありますが, 筆者はそうは感じません. 正直な処, 現状の仕様は隙だらけで到底安定運用に値しないからです. これら二つのブラウザはOSにバンドルされているもので, 有償サポートが行われていることから機能拡張については極めて保守的です. つまり, ある程度の設計品質が認められない限り, Web Componentsのサポートは一切為されないものとして考えてよいでしょう.

モジュール毎にサポート状況を確認する

Web Componentsのモジュール毎にサポート状況を確認するには次のコードを実行します.

サポートライブラリ

Web Components仕様に関わるスクリプトライブラリは既に存在しています. 以下に代表的なものを示します.

polyfillライブラリ

先ほど言及したようにWeb Componentsの機能はいずれも既存のAPIとJavaScriptで概ね実現出来るものばかりです. そのため, Web Componentsをサポートしない環境でも擬似的に動作させるためのJavaScriptライブラリ(polyfill)が提供されています.

  • webcomponentsjs
    Web Componentsを構成する機能を提供するライブラリで, かつてはplatform.jsと呼ばれていました. ネイティブ実装が存在するものはそのままに, Web Componentsとして足りない機能をスクリプトで補完します. なお, 必要な機能毎にライブラリを導入することも出来ます.

Web Components仕様を拡張する

下に挙げるライブラリにおいては, Web Components機構の上に独自のフレームワークを構成することで, 煩雑な手順を踏むこと無くWEB部品を定義できます. Web Componentsをサポートしない環境では上記のpolyfillライブラリと併用します.

  • Polymer
    Web Componentsに各種シンタックスシュガーを与えるなど, フレームワーク的な色合いの濃いライブラリです.
  • X-Tag - The Custom Elements Polylib
    カスタム要素の定義に特化したライブラリです.

この他にも多くのフレームワークがWeb Componentsのサポートを宣言しており, 今後様々な視点によるWEB部品の構成が可能となっていくことでしょう.

情報の入手先

Web Componentsは比較的新しい技術であることから, 信頼に値する文献に乏しいのが現状です. 従って最新の動向を探るためにも, なるべく公式に近いものを参照しましょう. (この文書も疑うくらいが調度よいでしょう. )

Web Componentsを学ぶ

Web Componentsは決して難しい概念ではありませんが, 実際の動作をイメージしにくいという欠点を持ちます. 従って座学に留めるのではなく, 実際のコードを自分で試してみましょう. ライブラリなどに頼らずとも高々数十行程度のHTMLを記述するだけで, 理解の度合いが変わります. また筆者の経験上, 本記事のようにtemplate要素から始めてCustom Elementsと読み進めていくと, 仕様が本来目指しているゴールがイメージしやすいかと思います.

template要素とは

template要素はHTML5で導入された要素で, 文書構造のひな形を定義する役割を持ちます. 一般にHTML文書を構成する場合, 大枠となる構造を定義しておき, スクリプトを使って後から内容を追加するということはよくあります. その際のひな形をHTML文書に埋め込むことが出来れば, そこにどのような構造が入りうるのかが明白となり, HTMLコードを管理しやすくなります. また, HTMLコードをエディタで編集する際にも役に立つでしょう.

しかし, これまでもテンプレート構造を文書に記述しておくことは慣例的に行われてきました. では, template要素はどのような点で有利なのでしょうか? そこでまず, 従来のテンプレート定義から順に見て行くこととします.

CSSを用いたテンプレートの定義

次の例ではテーブルの行を後から追加する目的でCSSを使って見えないtr要素を定義しています. テーブルに行を追加する場合は, この見えない行をコピーして元テーブルに挿入するようにします.

素直な内容ですがこれだけで十分テンプレートとしての機能を果たします. しかしこの方法では, HTMLだけでは見えないtr要素が何を表しているかが判りません. 処理の都合上単に非表示としているだけかも知れません. ではtr要素にclass属性を指定し, 行テンプレートであるという役割を明示してはどうでしょうか?

tr要素にラベルが付いたおかげで, この要素がどのような役割を持っているかを推測できるようになりました. しかしこの方法をもってしても, この部分だけでは実際にこの要素が非表示となっているのかが判りません. 判別するには別のstyle要素(もしくはlink要素)を参照せねばならないのです.

また何れにせよ余計なDOMの構成やスタイルの走査処理が発生している点にも注意してくだい. これらはテンプレート構造を無理矢理DOMツリーに定義しているために発生しているもので, 本来必要のないコストです.

このようにCSSを用いた方法は素朴ではあるものの幾つかの欠点も併せ持っています.

script要素を用いたテンプレートの定義

CSSによるテンプレート定義の問題を解決するために, これまでは実行されないscript要素がよく使われていました. type属性として「text/html」(もしくはそれに類するもの)を指定し, このscript要素の中にテンプレートとなる要素のソースを記述しておくのです. これはtype属性が「text/javascript, text/ecmascript, application/javascript, application/ecmascript以外の値だった場合, script要素が(JavaScriptとして)実行されないという仕様を逆手に取ったものであり, この方法であれば, 単にテンプレートをテキスト情報として保管しているだけですから, DOM展開に関わる処理が発生しません. テンプレートを挿入する際もtextContent/innerHTMLプロパティを複製するだけで済みます.

テンプレートのコピーを取得するためには次のようなコードを記述します.

このようにscript要素を用いた方法は現状でも十分実用的です. が, 欲を言えば明確な目的があるのですから, わざわざscript要素を流用するのではなくきちんとHTMLとして意味付けしたい処です. またテンプレートの複製を得るためにDOM構造へのパース処理が必要となるのは若干冗長に感じます.

template要素を使ったテンプレートの定義

そこでtemplate要素の出番です. template要素はそれ自体がテンプレートを表すものとして定義されています. また, 配下の要素についてはDOMツリー上には反映されず, 自動的にtemplate要素のcontentプロパティ(DocumentFragmentオブジェクト)内に展開されます. こうすることで, DOMの構成処理を最小限に保ち, テンプレートをコピーする際にも, いちいちパース処理を経ずとも既に展開されている構造をコピーするだけで済みます.

template要素の中身にアクセスする

template要素で定義されたテンプレート構造を利用するには次の手順を踏みます.

  1. テンプレートを格納しているtemplate要素(HTMLTemplateElement)を取得します.
  2. HTMLTemplateElementのcontentプロパティを複製します.
    contentプロパティの実体はDocumentFragmentオブジェクトなので, document.importNodeもしくはElement.cloneNodeメソッドを使うことでテンプレートの複製が得られます.
  3. その結果を加工しDOMツリーに追加します.

なおtemplate要素のツリー構造を加工せずにそのまま挿入する場合は, innerHTMLプロパティの内容を対象となるノードにコピーする方法もあります.

DocumentFragment:HTMLTemplateElement.content
テンプレート構造を取得します. 得られた内容は適宜複製して利用します.

テンプレートのロードのタイミング

テンプレートの生成はtemplate要素のロード時にのみ行われます. 従って, document.importNodeもしくはElement.cloneNodeを用いずにそのまま使ってしまうと, 次回使用時にそのテンプレートとなるcontentプロパティ(DocumentFragmentオブジェクト)の中身が空となってしまいます.


template要素をサポートしない環境への対応

このようにtemplate要素はテンプレート定義において有力な選択肢の一つですが, template要素をサポートしない環境(つまりInternet Explorer)ではテンプレートの内容がそのまま有効となってしまいます.

この場合, template要素を非表示としたり, テンプレートからscript/style要素を除外すると言った下準備に加え, 下記のようなpolyfillコードを導入すると言った工夫が必要となります.

id属性の扱い

HTMLにおけるid属性値は文書ツリー全体で一意の値を取ることとなっています. そのためtemplate要素を解釈しない環境を考慮し, template要素配下についても一意の値となるようにid属性を指定すべきです. また, 何度も複製を作る可能性があるものについては, そもそもid属性よりもclass属性をつけたほうがよいでしょう.

補足) template要素とパーシングの不確定性

とは言えtemplate要素も万能ではありません. template要素のパーシング(DOMツリーへの展開)処理はHTMLの中でも特殊なものです. そのため, 後々の処理のために特殊なツリー構造をtemplate要素に記述していた場合に, 実際のパース結果が意図したものと異なってしまう事があります.

一般にHTMLのパーシング処理は複雑であり, どのような記述が問題となるかを事前に把握しておくことは極めて困難です. そのため, あえてtemplate要素を使わないという選択もあり得ます.

HTML Importsとは

HTML Importsは外部リソースへの参照を表すlink要素を拡張し, スタイルシートやファビコン画像等に加えHTML文書の読み込みを可能とするものです. 同様のことは現状でもAjax機構を使って実現できますが, HTML Importsを利用することでより簡潔に記述できるようになります. 従来WEBブラウザ上で動作するフレームワークといえば大抵JavaScriptファイルとして提供されていましたが, 今後はHTML Importsのリソース集約機能を活用し, HTMLファイルとして配布される事が増えていくことでしょう. なお, スクリプトの実行が制限されている環境では動作しません.

文書のインポートとlink要素・style要素・script要素

rel属性にimportを指定したlink要素は外部のHTML文書をインポートする役割を持ちます. インポートされた文書(便宜上サブ文書と呼ぶこととします)は, ブラウザが描画しているメイン文書と区別されるため直接スクリーンに表示されることはありません. しかし内部的にメイン文書と同じwindowコンテキスト(環境)で展開されるため, 結果としてlink・style・script要素の内容がメイン文書とマージされます. 従ってテンプレートやCSSやスクリプト等の関連する設定を単一のファイルにまとめられます.

例えば次のようなHTML文書を作っておけば, それを利用する側ではhead要素にlink要素を1行付け加えるだけで済むようになります.

このようにHTML Importsを用いると, これまでは難しかった機能セットの観点からのコード集約が可能となるのです.

サブ文書をインポートしたlink要素の削除とスタイル

ではスクリプトを用いてlink要素をhead要素から削除したらどうなるでしょうか? この場合, サブ文書に含まれていたスタイル情報は削除されます. しかし既に動作済みのスクリプト処理はそのままとなります. つまりstyle要素やscript要素を個別に削除したケースと全く同じ結果となります.

インポート可能な文書の種類

HTML Importsでインポート可能な文書はHTML文書に限られます. プレーンXMLやSVG, MathML等のXMLデータはインライン形式で, その他の画像等のデータはBase64形式でエンコードしたデータURIスキーム形式に変換し, HTML文書に埋め込むことでインポートします.

サブ文書における外部リソースの参照

link, script, style要素を除き, サブ文書においてimg要素等が外部リソースを参照していたとしても, そのままではリソースは読み込まれません. template要素と同様にメイン文書にそのノードが挿入されてはじめてリソースが読み込まれます.

インポート可能な文書の範囲

XMLデータと同様にインポート可能なHTML文書はメイン文書と同じオリジンにある必要があります. なおこの条件はCORSによって軽減することが可能です.

インポートの連鎖

link要素が読み込んでいるHTML文書から更に他の文書をインポートすることが可能です. その際, 同一のHTML文書がインポート対象となる場合がありますが, HTML Importsではそのような文書を1度だけ読み込みます. 例えば次のようなインポート関係があったとします.

a.htmのlink要素(linkA)とb.htmのlink要素(linkB)が同じ文書・c.htmを参照しています. すると, 内部的にはlinkAとlinkBとは同じ(c.htmに相当する)documentオブジェクトを参照するようになります(暗黙の共有).

補足)サブ文書の集約

インポート関係をツリー化出来るとは言え, ドキュメント間に依存関係が存在するとリソースの管理が困難になります. また, 複数のファイルをインポートするのはサーバー負荷やネットワークトラフィックの観点からも避けたいところです. 事前に単一のインポートと出来ないか検討しましょう. コンポーネントの集約を行うVulcanizeと言ったツールもあるようです.

補足)HTML Importsの必要性に係る議論

Web Components仕様の中でもHTML Importsそのものは単なるリソースの取り扱いに関わる仕様であり, 既にpolyfillライブラリが存在するなど既存APIのみで(苦労すれば)十分に再現が可能です. そのため, WEBブラウザ側で新たな機能として提供すべきか否かについての議論が存在します.

HTML Importsが定義するAPI

HTML Importsの機能を実現するため, HTMLLinkElementに機能が追加されます. 以下にその内容を示します.

rel="import" href="[ドキュメントの参照先]"
インポートしたいドキュメントへの参照先を記述します. type属性は「text/html」として解釈します.
crossorigin=anonymous/use-credentials
CORSによるクロスオリジンでの読み込みを行う際の認証の有無を指定します.
anonymous…匿名, use-credentials…ユーザー認証を要する
async=true/false
サブ文書の読み込みを非同期で行うか. true…非同期, false…同期
Document:HTMLLinkElement.import
読み込んだ外部ドキュメントのdocumentオブジェクトが設定されます.

スクリプトを使ったインポート内容の表示

サブ文書内のコンテンツはメイン文書にコピー/移動することで利用します. 例えば次のようなHTML文書をインポートしたとします.

この内容をメイン文書に挿入するにはHTMLLinkElement.importプロパティを使ってサブ文書の内容をコピーします.

サブ文書のインポートとスクリプト

サブ文書にスクリプトを仕込んでおくことで, より複雑で高度な仕組みを実現できます. が, 効果的に利用するにはその動作順についてよく理解しておく必要があります.

インポート順とスクリプトの実行の基本

HTML Importsによるサブ文書のインポートは, 基本的にlink要素が現れた順に行われます. つまり, 複数のサブ文書をインポートしていた場合, 後続のサブ文書は既にインポート済みの文書の内容を全て利用することが可能です. 従って文書間に依存関係がある場合は, その順序に沿って文書をインポートするようにします.

スクリプトの実行順制御

動作パフォーマンスを踏まえサブ文書の読み込み処理は複数同時に行われ得ます. そのため, サブ文書に含まれているscript要素は先行している文書のインポート(つまりその中のスクリプトの実行)が完了するまで実行されません. こうすることで文書リソースに依存性を持つスクリプトの実行順序が保証されています.

サブ文書内でエラーとなるAPI

上記スクリプトの実行順制御の観点から, サブ文書内のscript要素において次のメソッドを実行しようとするとエラーとなります. メイン文書(document), サブ文書(document.currentScript.ownerDocument)の区別はありません.

  • document.open()
  • document.write()
  • document.close()

サブ文書とイベント

サブ文書の読み込みに関わるイベントとしては次の物があります.

  • link要素のload/errorイベント
  • サブ文書のDOMContentLoadedイベント

この内, 静的にインポートしているサブ文書のDOMContentLoadedイベントは, メイン文書のDOMContentLoadedイベントの発生直前にサブ文書の読み込み順に一括して発生します. そのため, メイン文書やサブ文書において各種documentオブジェクトのDOMContentLoadedイベントに任意の処理を定義することが出来ます.

サブ文書の読み込み成否をイベントで監視する

link要素はloadイベントとerrorイベントを発生させるため, このイベントに予め処理を登録しておくことでサブ文書の読み込みの成否に対処することが出来ます.

Function:HTMLLinkElement.onload
サブ文書の読み込みが完了した際に実行する関数を設定します. addEventListenerメソッドで処理を登録することも出来ます.
Function:HTMLLinkElement.onerror
サブ文書の読み込みにエラーがあった際に実行する関数を設定します. addEventListenerメソッドで処理を登録することも出来ます.

動的にサブ文書を読み込む

link要素が読み込まれるとサブ文書が利用可能となるまで後続のスクリプト処理がストップします. 従ってサブ文書の読み込みに時間が掛かると, ページ全体としての動作パフォーマンスを著しく損なうことがあります. この問題を避けるにはasync属性(にtrue)を指定しサブ文書を非同期的に読み込むか, スクリプト中でHTMLLinkElementを生成し, ロードタイミングを調整するようにします. この場合, サブ文書の読み込み完了をイベントで捕捉することになります. なお, img要素等と異なり, link要素はhead要素に挿入して初めて効果を発揮します. 従って正しくイベントを発生させるにはDOMに挿入します.

サブ文書を操作するためのAPI

従来のHTMLスクリプティングではメインとなるdocumentオブジェクトは唯一の存在でしたが, HTML Importsの導入によりメイン文書とサブ文書の複数のdocumentオブジェクトを操作する必要があります. そのため, スクリプトを記述する際はどの文書を操作しているのかを意識する必要があります.

スクリプトの実行コンテキスト

先に見た通り, link要素で読み込んだHTML文書はメインとなる文書と同じコンテキストで展開されるため, そこに含まれるスクリプトはメイン文書に埋め込まれているかのように振舞います. そのためwindowやdocument等のグローバル変数はメイン文書でのスクリプトと共有されます.

スクリプトと所有者ドキュメント

すると, サブ文書内のスクリプトからサブ文書そのものにアクセスするにはどうすればよいのでしょうか? 通常サブ文書はメイン文書の内容を知りませんから, どのlink要素で読み込まれているかを判断することが出来ません. 従ってHTMLLinkElement.import以外の方法を探る必要があります.

この問題はDocument.currentScriptとElement.ownerDocumentを組み合わせることで解決します.

HTMLScriptElement:Document.currentScript
現在のスクリプトが記述されているscript要素を取得します.
Document:Element.ownerDocument
要素が属するドキュメントを取得します.

図に表すと次のようになります.

つまり, サブ文書中のスクリプトが実行されている最中に「document.currentScript.ownerDocument」を参照することで自身が所属しているサブ文書を参照することが可能となるのです.

イベント処理と所有者ドキュメント

但し, イベント処理等からサブ文書の内容を取得したい場合は, ownerDocumentを何らかの変数に保存しておく必要があります. なぜなら, イベント処理はscript要素で実行されているわけではないため, 実際のイベント処理時にcurrentScriptプロパティの値がnullとなるからです. なお, 上記の例のようにscript要素直下で変数を定義するとこれはグローバル変数として扱われます. 従ってグローバルスコープの汚染を防ぐ場合は, 一旦無名functionを定義し, その引数としてdocumentオブジェクトを渡すようにします.

polyfillライブラリとcurrentScriptプロパティ

HTML Importsをサポートしない環境ではdocument.currentScript.ownerDocumentプロパティは常にwindow.documentオブジェクトに一致します. これはpolyfillライブラリを導入したところで変わりません. 言語仕様上, プロパティ値を上書き定義することが出来ないからです. そのためpolyfillライブラリでは_currentScriptプロパティ等を新たに定義し, HTML Imports環境でのcurrentScriptプロパティの代わりとしています. 従って, polyfillライブラリの有無を考慮するのであれば, 実行中のscript要素を得るには次のコードを用いることになります.

メイン・サブ文書間のノード受け渡し

メイン文書とサブ文書に相当するdocumentオブジェクトは, 単一のコンテキスト(window)を共有することから何れもwindow.Documentインターフェースのインスタンスです. 従ってその間では自由にノードの受け渡しが可能です. つまり, 異なるdocument間でノードの授受を行う際に, document.importNodeメソッドを介さずとも良いのです.

サブ文書として開かれている時のみスクリプトを実行する

HTML ImportsはHTML文書をインポート対象とするため, 本来サブ文書として開かれるべきものを直接ブラウザで開くことができます. この時, アクセスログの収集スクリプト等をサブ文書に集約していた場合, 意図しないログが発生することになります.

この問題をクライアントサイドで解決したい場合は, サブ文書の読み込み時の先頭でコンテキストのドキュメントとscript要素の所属ドキュメントとを比較し, 一致した際に後続の処理をキャンセルすることで対応します.

サブ文書にパラメータを渡す

サブ文書におけるdocument.URLプロパティを参照することで, サブ文書のURLを取得できます. 従って, URLに挿入されているクエリ文字列を解析することで, メイン文書からサブ文書にパラメータを渡すことが可能です.

HTML Importsと相対パス

サブ文書中に記述された相対パスはサブ文書のパスを基準に解釈されます. 従ってメイン文書に外部リソースを挿入するケースにおいては, リソースへのパスをどのように記述しておくかが問題となります. この方法としては(1)「メイン文書からみた相対パスで記述しておく方法」と(2)「サブ文書から見た相対パスで記述しておく方法」の二つが考えられます.

ここで後者を選択した場合は, メイン文書にパス情報を渡す前に相対パスから絶対パスへの読み替えが必要です.

Shadow DOMとは

HTMLではその仕様上, データの構造がそのままスクリーン表示時の構造となります. これは非常にシンプルで理解しやすい考え方でしたが, 表現力に乏しいという面も持ち合わせています. この問題はCSSを用いることである程度までは解決するのですが, あらゆるパターンの要件を充たすには機能不足だったため, 見た目を制御するためだけの要素を用意し, 複数の要素を組み合わせて利用することがよく行われています.

しかしこの方法では, 単一のDOMツリーに多くの要素がぶら下がることとなるため, HTMLコードやDOM構成の可読性を損ないます.

Shadow DOMはスクリーンに表示される要素(ツリー)ごとに「意味的な構造(model)」を「見た目の構造(view)=シャドウツリー」に再構成することで, 描画内容を自由に指定可能とします. これはCSSによるスタイルの指定よりもはるかに柔軟に行えるため, 元となるデータ構造をシンプルに保つことが可能です. またシャドウツリーに対しては個別にスタイル付けが可能で, 通常煩雑となりがちなid/class属性の付与やCSS記述を簡略化できます.

Shadow DOMの外観図

その一方でHTML仕様を横断的に拡張していることにより, DOMが本来持っているシンプルさを損なう結果を招いており, 全体像を把握するのが非常に困難になっています. そのため魅力的な機能ながら, 実際の設計・実装フローに組み込むことが難しいといった特徴を持つ, 言わば諸刃の剣です.

補足)仮想DOMとの関係

用語的に似ている概念として仮想DOM(virtual DOM)がありますが, Shadow DOMとは全く異なるものです. DOM構造に見立てたオブジェクト(=仮想DOM)に対する操作を監視し, 変更箇所のみをDOMに反映させることでDOM操作に懸かるコストを軽減します.

従って, 本質的でない部分をシャドウツリーとして隠蔽するShadow DOMとは概念的に連携可能です. 図にすると次のようになります.

なお, 現在仮想DOM機構を提供する標準的な仕組みは存在せず, 専用のライブラリを導入する必要があります.

Shadow DOM利用の二つのパターン

Shadow DOMの利用に制限はありませんが, 概ね次の二つのパターン, 及びその組み合わせにおいて効果的です.

Shadow DOMでレイアウト構造を与える

Shadow DOMの使い方その1

複雑化したWEBページ構造をShadow DOMでテンプレート化し, 本質的なコンテンツのみをページに残す方法です. これはHTMLでのフレーム機構, CSSにおけるメディアクエリをより強力にしたものと考えてよく, デザインとコンテンツとを分離することで管理を容易にする他, テンプレートを書き換えることで環境毎に最適な出力を得ることも可能となります.

Shadow DOMによる機能の挿入

Shadow DOMの使い方その2

既存の要素に新たな構造・機能をShadow DOMで挿入するものです. これまで::before, ::after擬似要素を用いて無理矢理スタイル付けしていた部分に, ボタン等のより具体的な機能を追加することが可能になります. また, 複数のコントロールを一つにまとめて単一のコントロール(カスタムコントロール)とする場合もこちらのケースに分類できるでしょう.

いずれのケースにおいても, Shadow DOMによって挿入される構造が元のツリーと分離されることで, 容易に再利用可能となっている点に注目しましょう.

Shadow DOMの乱用に依る弊害

Shadow DOMによる構造の隠蔽は非常に魅力的ですが, かと言って無意味に使って良いものではありません.

動作パフォーマンスへの影響

HTMLコード上から見えないと言っても, スクリーンへのレンダリング処理時にシャドウツリーの構造を展開することになるため, 未使用時よりも確実に動作パフォーマンスが低下します.

Shadow DOMそのものが持つ複雑性の問題

Shadow DOM仕様はツリー構造の再編成に関わる仕様であり, 理屈上は幾らでも複雑な構造を採ることが可能です. そのため, 無計画にシャドウツリーを生成すると全体としてのツリー構造がどのような経緯で生成されているのかが見えにくくなります.

以上から, スタイル設定だけでは対処出来ないような場合に限りShadow DOMを使うと言ったように限定的に利用すべきでしょう.

Shadow DOMの動作例

最も簡単な例を示します. Shadow DOM機構を用いてdiv要素の中身と異なる文字列をスクリーンに表示しています.

次の例は. a要素をそのままにリンク先の画像を挿入したものです. a要素のcreateShadowRootメソッドで生成したShadowRootオブジェクトに, シャドウツリーを定義しています.

このように, 元となるDOM構造にいっさい手を入れずに, 見た目の構造を自由に注入することが可能となります.

template要素との連携

一般にシャドウツリーの構造は複雑になりますが, template要素を使うことでツリー構造をそのままShadowRootオブジェクトに挿入することが可能です.

こもんちゃん

comon@xxx

よろしくね!

補足)template要素を使わないという選択

template要素を使った場合, 意図した通りにDOMツリーが構成されないケースがある事は示しました. この場合, innerHTMLプロパティを直接操作することで正しく動作するようになります. 下はcontent要素とtr要素をシャドウツリーの中身として利用していますが, 現状template要素を用いるとこの構造が破壊されてしまい, 正しく動作しません.

列1列2
行11040
行22050
行33060

Shadow DOMにより拡張されるAPI

先に見た通り, Shadow DOMはこれまで単一だった子孫ノードツリーの概念を大幅に拡張しています. 以下にShadow DOMによって拡張されるAPIを示します.

ShadowRootオブジェクト

ShadowRootオブジェクトはShadow DOMを構成する上で中核となるオブジェクトです.

ShadowRoot:DocumentFragment
Elementに対するシャドウツリーのrootを表すオブジェクトです. DocumentFragmentをベースとしています.
Selection:ShadowRoot.getSelection()
シャドウツリー内の現在選択されている範囲を取得します.
Element:ShadowRoot.elementFromPoint(x, y)
シャドウツリー内の指定した座標における要素を取得します.
Element:ShadowRoot.activeElement
シャドウツリー内のアクティブなElementを取得します.
Element:ShadowRoot.host
このシャドウツリーを所有しているElementを取得します.
ShadowRoot:ShadowRoot.olderShadowRoot
1履歴前のShadowRootを取得します.
DOMString:ShadowRoot.innerHTML
ShadowRootの内容を指定・取得します
StyleSheetList:ShadowRoot.styleSheets
シャドウツリー中に適用されているスタイルシート(style要素)のリストを取得します.

HTMLに新たに追加される要素

Shadow DOMはHTMLにcontent,shadowの二つの要素を追加します.

content要素
シャドウツリー内のコンテンツの挿入箇所を表します.
select属性
挿入するコンテンツへのセレクタを指定します.
DOMString:HTMLContentElement.select
select属性に対するアクセッサを与えます.
NodeList:HTMLContentElement.getDistributedNodes()
コンテンツとして挿入されている要素のリストを取得します.
shadow要素
シャドウツリー内のシャドウ挿入ポイントを表します. これはシャドウツリーの履歴表示に用いられます.
NodeList:HTMLShadowElement.getDistributedNodes()
シャドウ挿入ポイントに挿入されている要素のリストを取得します.

Elementオブジェクト

Shadow DOMはシャドウツリーを操作するためにElementオブジェクトを拡張します.

ShadowRoot:Element.createShadowRoot()
現在のElementにおけるシャドウツリーを生成します.
NodeList:Element.getDestinationInsertionPoints()
現在のElementに対するシャドウツリーの挿入ポイントを取得します.
ShadowRoot:Element.shadowRoot
現在のElementにおけるシャドウツリーのrootを取得します.

セレクタ

シャドウツリーへのアクセスをCSSから行うために, Shadow DOMはセレクタを拡張します. Shadow DOMに関わるセレクタの拡張はCSS Scoping Module Level 1, Selectors Level 4で与えられます.

::shadow
ShadowRootを参照する擬似要素セレクタ.
::content
content要素で定義されている挿入ポイントを表す擬似要素セレクタ.
:host
シャドウツリー内から, 当該シャドウツリーを所有している要素にマッチする擬似クラスセレクタ. (:rootではない)
:host([selector])
:hostが指定した条件を充たす際に:hostにマッチする擬似クラスセレクタ.
:host-context([selector])
:hostの祖先のうち, 指定した条件を充たす要素が存在した際に:hostにマッチする擬似クラスセレクタ.

メインツリー/シャドウツリーの区別を行わないもの

>>>
シャドウピアシング子孫コンビネータ(shadow-piercing descendant combinator)と呼びます. 後続の走査処理をメインツリー/シャドウツリーの区別をせず全てに行います. かつては/deep/と記述しておりdeepコンビネータと呼んでいました.

Eventオブジェクト

シャドウツリーはイベントバブリングの仕組みに影響するため, Eventオブジェクトが拡張されます.

NodeList:Event.path
イベントが伝達された経路を取得します.

シャドウツリーの機能

一般にHTML文書を構成する要素はhtml要素をルートとするツリー構造を採ります. ここでツリーを構成するノードの何れかでcreateShadowRootメソッドを実行すると, そのノードに対してShadowRootオブジェクトが生成され, スクリーンに描画するための新しいツリー(シャドウツリー)が作られます. シャドウツリーが生成されると当該要素を描画する際に, 子孫要素の代わりにシャドウツリーの内容が使われるようになります.

ここで注意すべきは, いかなシャドウツリーを使ったといえどCSSによるDOMの描画はこれまでと全く同じルールで行われるということです. 単にDOMツリーを手繰る際に, 実ツリーではなくシャドウツリーが使われるようになるだけで, CSSへの影響は(セレクタの記述が特殊になる以外は)特にありません.

シャドウツリーへのアクセッサ

createShadowRootメソッドで作成されたShadowRootオブジェクトはElement.shadowRootプロパティから参照出来ます.

シャドウツリーを生成した要素の振舞い

createShadowRootメソッドを実行した要素は, 見た目上子要素がなくなったように振る舞います. しかし, 要素そのものは描画対象となります. つまりdiv要素であればborder等の要素を囲むスタイルで描画されます.

ShadowTree未生成

ShadowTree生成

シャドウツリーへの要素の挿入

createShadowRootメソッドで生成したShadowRootオブジェクトはシャドウツリーのルートノードに相当するものです. ShadowRootオブジェクトに要素を追加すると, シャドウツリーに要素を追加したことになります. ShadowRootオブジェクトはDocumentFragmentインターフェースを実装しているので, 通常のElementと全く同じ感覚で中身を操作することが可能です.

ShadowTree未生成

ShadowTree生成

シャドウツリーと描画範囲

iframe要素やobject要素を用いた場合, 描画されるコンテンツはそれを読み込んでいるノードの描画範囲に収まりますが, シャドウツリーについては必ずしもそうとは限りません. シャドウツリー内のノードについても, CSSレイアウト上はメインツリーに存在するものとして扱われるため, コンテンツに記述されているスタイルによっては, シャドウツリーを保持しているノードの範囲外に描画されることもあります.

シャドウツリーに挿入されたscript要素

シャドウツリー内にscript要素を挿入した場合, 通常の場合と同様にスクリプトが実行されます. またこの時, スクリプトは元ツリーが属するdocumentを開いているwindowオブジェクトをグローバルスコープとします.

下はtemplate要素配下にscript要素を配置し, テンプレート挿入時の初期化を行っています.

シャドウツリーと相性の悪い要素

シャドウツリーにはその役割上, あくまでスクリーン描画のための要素を挿入します. そのため, 描画に直接関わらないlink要素やbase要素と言った, 主にhead要素にのみ含まれる要素はシャドウツリーに挿入しても動作しません. また, select要素配下のoption要素など, レイアウト作用以外に要素の親子関係がその動作に強く影響するものをシャドウツリーに挿入した場合, シャドウツリーを介することでその結びつきが分断されるため, 動きそうに見えてその実正しく動作しないと言った奇妙な現象を招きます.

このような関係にある要素には次のものがあります. 全てを確認したわけではありませんが, これらの要素にシャドウツリーを適用するのは避けたほうが無難でしょう.

  • select-option
  • form-input
  • object-param
  • picture-img
  • video-source
  • map-area
  • SVGに関わる要素(gやsvg等のコンテナ要素を除く)

その他にも仕組み上子要素を持ち得ない(持ったとしてもフォールバックコンテンツとして表示されない)要素, 例えばmeter要素, progress要素に対しても仕様上はシャドウツリーを持ちうるとされていますが, 条件によっては例外を発生する事があります.

シャドウツリーの削除

一度生成したシャドウツリーを削除するAPIは存在しません. そのため, 当該要素を再生成して置き換えるようにします.

当該要素にイベントが登録されていたり生成に複雑な処理を要する場合は, シャドウツリーの中身をcontent要素で書き換えることで擬似的にシャドウツリーの機能を無効化することが出来ます.

なお, 既にあるシャドウツリーを初期化する目的でcreateShadowRootメソッドを利用してはいけません. なぜなら, シャドウツリーは履歴管理されており, 最初のシャドウツリーは依然メモリ上に残っているからです.

シャドウツリーの隠蔽

一度生成したシャドウツリーを隠蔽し, 外部からの変更を受け付けないようにする(完全なカプセル化)ことはJavaScriptの仕組み上できませんが, オブジェクトのプロパティを上書きすることでその頻度を軽減することは可能です.

シャドウツリーとDOM操作

これ以外にも考慮すべき特徴は存在します.

要素のクローン

cloneNodeメソッド等を使って要素の複製をしたとしてもシャドウツリーまでは複製されません.

シャドウツリーとシリアライズ

シャドウツリーの内容はinnerHTMLプロパティやXMLSerializerなどによる文字列へのシリアライズ処理の対象外です.

シャドウツリーとcontenteditable属性

contenteditable属性はシャドウツリーの内容に影響を及ぼしません.

シャドウツリーとMutationObserver

シャドウツリーへの変更を検知するにはMutationObserverオブジェクトを使って, ShadowRootオブジェクトを監視するようにします. なお次のように記述すればメインツリー・シャドウツリーの両方に対する変更を一括して監視出来ます.

シャドウツリーとスタイルシート

シャドウツリーの導入に伴い, スタイルシートの扱いも拡張されました.

CSSスコープの分離

シャドウツリーにはメインとなるCSSスコープにぶら下がる子CSSスコープが定義されます. 子スコープから親スコープのスタイルを変更することは出来ません. 従ってページ全体への影響を考えること無く自由にスタイルを定義することが出来ます.

逆に親スコープからはフォント設定などのツリー構造を辿って継承される物以外はスタイルが未設定となります. これは一般的なセレクタは子スコープに影響を及ぼさないからです. 従って意識的にスタイル付けをしない限り, シャドウツリー内の要素にはスタイルが設定されないことになります.

このようにメインツリーとシャドウツリーとの間では明確なスタイル設定の境界が存在します. しかし, 使い勝手を鑑みると必要に応じてこの境界を超えたスタイル設定が欲しくなるところです. そこでShadow DOMではセレクタを拡張してこの境界を超えるための仕組みを定義しています. 詳しくは後述します.

シャドウツリーへのスタイル定義

シャドウツリー内部へのスタイル定義には原則style要素を用います. なお, シャドウツリーにはhead要素に対応する仕組みが存在しないため, link要素を使ったスタイルシートの外部参照は出来ません. もしどうしても必要であれば@import命令を利用します.

シャドウツリーへのコンテンツの挿入

ここまでは, 既存のツリーにシャドウツリーを挿入する事を見てきましたが, これとは逆に既存の子要素をシャドウツリーに挿入(投影)する仕組みが提供されています.

content要素を用いたコンテンツの挿入

content要素を用いると, シャドウツリー内の任意の位置にツリーを生成したノードの内容もしくは子要素を挿入(投影)することが出来ます.

content要素による投影

次の例では, div要素内のinput要素をシャドウツリーで作ったfieldset要素に挿入しています.

この時, シャドウツリー内のcontent要素の位置を挿入ポイントと呼びます.

見た目上content要素の部分に要素が存在しているように見えますが, content要素そのものは中身を持ちません. また, このことからcontent要素はシャドウツリーを持ちません.

なお, 挿入されている要素のリストを得るにはgetDistributedNodesメソッドを実行します.

挿入に条件を付ける

content要素にselect属性を指定すると, その条件に合致した要素群のみを挿入します. select属性には挿入したい要素に対するセレクタ(カンマで列挙可能)を指定します. 次の例はシャドウツリーに複数の挿入ポイントを定義した例です. 挿入ポイントごとに異なる検索条件を設定しています.

要素の挿入には次のようなルールが定められています.

  • シャドウツリーに挿入可能な要素はシャドウツリーを所有している要素の子要素のみです. なぜなら子孫を列挙して挿入可能とするとツリー構造が破壊されてしまうからです.
  • 挿入処理はcontent要素が現れた順に実行されます. 挿入対象はselect属性に記述されているセレクタを元に決定されます. セレクタが未指定の場合は全ての子要素が単一のcontent要素に挿入されます.
  • 逆に何れかのcontent要素に挿入されない限り, 子要素はスクリーン上に表示されることはありません.
  • 一度挿入された要素は後続のcontent要素による検索の対象から外れます. 従って, 一つの要素が複数回シャドウツリーに挿入されることはありません.

このようにツリーを加工すると, いかなるシャドウツリーを定義したとしても最終的に得られるビュー構造も(ループなどを含まない)完全なツリーになります. この特徴はShadow DOMを矛盾無く定義する上での肝となるものです.

select属性に指定できるセレクタ

select属性に指定可能なセレクタは以下の通りです. 条件に合致しない記述をした場合, 未設定時(つまり, ユニバーサルセレクタ単体)と同じ動作となります.

  • ユニバーサルセレクタ(*)
  • タイプセレクタ(div, span, input…)
  • クラスセレクタ(.class, .some…)
  • IDセレクタ(#id)
  • 属性セレクタ([readonly], [type="button"]…)
  • 否定擬似クラス(:not(.class))

select属性を変更する

content要素によるノードの挿入はスタイルシード等の影響を受けません. 従って, 下のように何らかの状態によって挿入するノードを変更する場合は, スクリプトを記述する必要があります.

シャドウツリー構造を変更する

シャドウツリーの構造は後から変更することが出来ます. 従って予めテンプレートを複数用意しておき, 都度シャドウツリーの中身を書き換えることで自由にレイアウトを変更することができます.

特殊な挿入が行われる要素

シャドウツリーを持つ要素の種類によっては特殊なコンテンツの挿入が行われることがあります.

挿入ポイントの一般化

シャドウツリーにおける挿入ポイントの考え方を一般化すると既存のHTML要素においても, その要素の直下がコンテンツの挿入ポイントと考えることが出来ます. そうすると, 要素によっては挿入ポイントを複数持つものが存在します. その典型がfieldset要素です. fieldset要素には配下のlegend要素が自動的に見出しの位置に表示されますが, これは一般の要素向けの挿入ポイントに加えてlegend要素専用の挿入ポイントが存在している事になります.

fieldset要素とコンテンツの挿入

ではfieldset要素に対してシャドウツリーを定義した場合, どのようにコンテンツの挿入が行われるのでしょうか? 答えは簡単で, シャドウツリーを持たない場合と同様にノードの挿入が行われます. つまり, (template要素のselect属性で明示されない限り)legend要素は他の要素と区別され, 自動的に規定の挿入ポイントに充てがわれます.

要素の挿入とスタイル

content要素でシャドウツリー内に(投影)された要素は, メインツリーでのスタイルで描画されます. 詳しくはセレクタの項で解説します.

シャドウツリーの生成履歴とshadow要素

シャドウツリーは複数生成することが可能で, 内部で生成履歴が管理されています. これはシャドウツリーに更にシャドウツリーを挿入するためのもので, シャドウツリー中にshadow要素を指定すると前回生成したシャドウツリーをその位置に挿入することが出来ます. このshadow要素の位置をシャドウ挿入ポイントと呼びます. shadow要素を使うとデザインパターンで言うところのデコレーション機構を実現できます.

複数のshadow要素がシャドウツリー内に存在した場合は先頭の要素以外は無効となります. これはcontent要素による要素の挿入が一箇所に限るのと同じです.

シャドウツリーの履歴をたどる

Element.createShadowRootメソッドを実行すると, Element.shadowRootプロパティには最新の(youngest)ShadowRootオブジェクトが, ShadowRoot.olderShadowRootプロパティには一履歴前のShadowRootが格納されます. これらのプロパティをたどることで, シャドウツリーの生成履歴が得られます.

シャドウツリー履歴とコンテンツの挿入

content要素に対するノードの挿入処理は新しいシャドウツリーから順に行われます. つまり, 最新のシャドウツリー内のcontent要素に挿入されなかった子ノードは, 条件に合致するcontent要素が見つかるまでシャドウツリーの生成履歴を古い方へ辿っていきます. 結果何れのcontent要素の挿入条件とも合致しなかったものは非表示となります.

シャドウツリーの分析

シャドウツリーの構造は単一のcontent要素からなるものから, 複数のツリーをshadow要素を使って複雑に組み合わせて作られるものまで多種多様です. そのため, その内容を効率的に分析するためのAPIが定義されています.

表示上の親ノードを取得する

親ノードがシャドウツリーを持っていた場合, そのノードは(content要素があれば)シャドウツリーの配下に存在するかのように振舞います. 従ってあるノードに対しては, データ構造上の親ノード(parentNode)と表示上の親ノード(content要素)の二つの親を持つことになります.

getDestinationInsertionPointsメソッドはこの表示上の親ノードを取得します. なおシャドウツリーが複数作られており, そのcontent要素が更に別のshadow要素に直接挿入されていた場合は, 更にそのshadow要素をも抽出することが出来ます. そのため, メソッドの実行結果がNodeList形式をとっています.

挿入されているノードをリストアップする

逆にcontent要素及びshadow要素に対してgetDistributedNodesメソッドを実行すると, 当該要素に挿入(投影)されているノードのリストが得られます. 下は表示上のツリー構造をテキストデータとして出力するものです. shadow要素には1履歴前のshadowRootオブジェクトの内容が設定されていることが判ります.

このように, ビュー構造に対してgetDistributedNodesはトップダウンの, getDestinationInsertionPointsはボトムアップの分析を行います.

ノード関係のまとめ

以下にノードの関係を示します.

黄色いノードについて, 実ツリー上の親子関係(青色), シャドウツリーにおける親子関係(赤色, 親シャドウツリーにおける挿入ポイント, 所有シャドウツリーのルートノード)の4つの関係があり, それぞれにアクセスするための仕組みが存在している事が判ります.

シャドウツリーとセレクタ走査

Shadow DOMはその構造上メインとなるDOMツリーに更なるシャドウツリーを追加します. そのため, 既存のセレクタだけでは円滑なノード検索が行えません. そこでShadow DOM専用のセレクタが新たに追加されています.

content要素とshadow要素へのスタイル指定

content要素及びshadow要素はノードもしくはツリーの挿入ポイントとしての意味合いを持つだけであり, スクリーンへの描画処理上は実体を持ちません. 従ってスタイルを指定しても無効となります.

::shadow擬似要素

::shadow擬似要素はあるノードが保持しているシャドウツリーのルートノードを参照します. このセレクタを利用することでメインツリー上のstyle要素やscript要素からシャドウツリー内部にアクセスすることが可能となります. この擬似要素はシャドウツリーの外側から内側を検索する際に利用します.

::content擬似要素

::content擬似要素はcontent要素で定義された挿入ポイントを表します. content要素は子要素を持ちませんが, ::content擬似要素の配下には挿入ポイントに挿入された要素が存在するものとして考えます. その結果, ::content>*と記述することでシャドウツリーに挿入されている要素の全てを得ることが可能です. この擬似要素はシャドウツリーの内部から外側の要素を得るために利用します.

:host擬似クラスとそのバリエーション

:host, :host(), :host-context()擬似クラスはシャドウツリー内でのみ有効なセレクタで, スタイル情報をシャドウツリー内部に隠蔽(カプセル化)する上で重要な役割を果たします.

:host擬似クラス

:host擬似クラスは当該シャドウツリーを所有している(つまりシャドウツリー外の)要素(ホストノード)を参照します.

:host擬似クラスはその特性上他のセレクタと組み合わせると正しく動作しません. 通常セレクタはAND条件で掛け合わされます. シャドウツリー内部では, :host以外のセレクタはシャドウツリー内の要素にのみマッチします. するとホストノードはシャドウツリーの外に存在するため, 組み合わせて利用すると常に何も返さなくなるのです. 従って, シャドウツリー内部からホストノードにマッチ条件を付加する場合は後続の:host()擬似クラスセレクタを用います.

:host()擬似クラス

:host()擬似クラスは:host擬似クラスに条件を付けたものです. 引数に指定したセレクタに合致した場合にのみシャドウツリーのホストノードにマッチします. このセレクタを用いることで, ホストノードの状態(:checked, :focus, :active等)をシャドウツリー内部から検知することが可能になります.

:host-context()擬似クラス

:host-context()擬似クラスは:host擬似クラスに条件を付けたものです. ホストノードに対して引数に指定したセレクタに合致する祖先が存在する場合にのみホストノードにマッチします. これはメインツリーの状況をシャドウツリー内部から読み取って内部スタイルを書き変える(例えばテーマ設定)といった用途に使います.

スタイルの優先順

:host擬似クラスによるホストノード, ::content擬似要素による挿入されたノードへのスタイル付けが可能であることから, ある要素に対するスタイルは次の3つの場所で定義できることになります.

  1. style属性によるスタイルの指定
  2. メインツリー内のstyle要素によるスタイルの指定
  3. シャドウツリー内のstyle要素によるスタイルの指定
    (:host擬似クラスによるホストノードへの, ::content擬似要素による挿入されたノードへのスタイル指定)

この内, シャドウツリー内で与えたスタイルはメインツリーから与えられたスタイルで上書きすることが可能です. これは後述するカスタム要素に対するデフォルトスタイルを定義するための仕組みと考えると理解しやすくなります.

main

main+shadow

main+shadow(important)

shadow

シャドウツリーのルートを取得する

:root擬似クラスセレクタは常にメインツリーを参照してしまうため, シャドウツリーのルートノードを取得する用途では使えません. この場合, :host擬似クラスセレクタに::shadow擬似要素セレクタを組み合わせることで, シャドウツリーのルートノード(shadowRootに対応するスクリーン上では意味を為さないノード)を取得できます.

要素がシャドウツリーに含まれているか確認する

このシャドウルートを取得するセレクタ記述を用いると, 当該要素がシャドウツリー内に含まれているかを確認することが出来ます.

但し, 上記の方法は動作が曖昧なため, ツリーを遡ってShadowRootに突き当たるかを判定したほうが確実でしょう.

シャドウピアシング子孫コンビネータ(deepコンビネータ)

セレクタに>>>を記述することで, 後続するセレクタがメインツリー・シャドウツリーの区別なくマッチするようになります. これをシャドウピアシング子孫コンビネータと呼びます. この記述は非常に強力な反面, スタイルのカプセル化を破壊する事もあります.

シャドウツリーとイベント

UIを構成する上で必須となるイベント処理もシャドウツリーの挙動に合わせて仕様が拡張されています.

シャドウツリーとイベントパス

シャドウツリーで構成したツリー内で発生したイベントは見た目上のツリー構造を辿って伝播します. 言い換えると, DOMツリーのルートノードからイベントを伝達している途中にcontent要素によるノードの挿入が存在していた場合, イベントはあたかもcontent要素と挿入対象のノードとの間に親子関係があるかのように伝播していきます.

例を示します.

これを図で表すと次のようになります.

ここではノードBがシャドウツリーを所有しており, 挿入ポイントGにBの子ノードCが割り当てられています. この時, ノードDでイベントが発生したとすると, まずノードDからルートノードまでの経路(イベントパス)が算出されます. その際ノードCと挿入ポイントGとの間には仮の親子関係が存在するものとして扱われるため, この例におけるイベントパスは「D→C→G→F→E→B→A(…→root)」となります.

するとイベント処理はこのイベントパスを基準に行われます. つまりイベントのバブリングフェーズではこの経路の順にイベントが伝わっていきます. また当該イベントにキャプチャリングフェーズがあればこの経路の逆「(root→…)A→B→E→F→G→C→D」にイベントが伝播していきます. なおイベントパスはリスナ関数に渡される引き数のEvent.pathプロパティで取得できます.

何れのイベントも基本的にこの動作をとりますが, 中には特殊なイベント伝播をとるものがあります.

MutationEvents

DOMへの変更を監視するMutationEventsはビュー構造と関連しないため, 上記シャドウツリーを介したイベントバブリング処理の対象外です.

メインツリーへの伝播が行われないイベント

イベントの内下記のものはシャドウルートからメインツリーへのイベント伝達(先程の例ではE→B)が一切行われません.

  • abort
  • error
  • select
  • change
  • load
  • reset
  • resize
  • scroll
  • selectstart

イベントのリターゲティング

Event.targetプロパティには通常イベントを発生させた要素が設定されています. が, シャドウルートの中からメインツリーにイベントが伝達される際に, シャドウツリーの内容をカプセル化する目的でtargetプロパティがシャドウツリーを所有するノードに読み替えられる事があります. これをイベントのリターゲティングと呼びます.

先程の例ではHノードでイベントが発生すると, EノードまではHノードがEvent.targetに設定されますが, Bノード以降はEvent.targetがBノードに設定されます.

focus, blur, foucusin, forcusout, touchイベントにおいてリターゲティングが発生します.

relatedTargetのリターゲティング

イベントの中にはイベントを発生させたノード(target)の他に, イベントを発生させるきっかけとなったノード(relatedTarget)が設定されるものがあります. mouseenter, mouseleave, mouseover, mouseoutイベントがこれにあたります.

relatedTargetはtargetと同じツリー上のノードが選択されます. 見た目上, シャドウツリー内のノードがイベント発生のトリガとなっていたとしても, シャドウツリー内のノードがrelatedTargetに設定されることはありません. これをrelatedTargetのリターゲティングと呼びます.

下では, ノードBは見た目上ノードCに囲まれており, mouseoverイベントはノードB<=ノードCと言った関係で実行されそうですが, ノードBはシャドウツリーの内部構造を知らないため, ノードCがノードAにリターゲティングされるのです.

シャドウツリーに係るその他のAPI

Documentオブジェクトが提供するAPIは基本的にメインツリーに対する操作であり, シャドウツリー内部の状態までは関知しません. 従ってシャドウツリーに同様の操作を施すため, ShadowRootオブジェクトには幾つかDocumentオブジェクト等と同名のAPIが備わっています. 但し, これらはシャドウツリーの構築から副次的に生まれたものであり, 現状ブラウザにおける動作が芳しくありません.

シャドウツリー内のスタイルシートを列挙する

シャドウツリーに挿入されたstyle要素は, 原則そのシャドウツリーでのみ有効なスタイルシートを定めます. 従って, シャドウツリー毎のスタイルシート設定を取得するためのプロパティShadowRoot.styleSheetsプロパティが定められています. 使い方はDocument.styleSheetsプロパティと全く同じです.

シャドウツリー内のアクティブな要素を取得する

Document.activeElementプロパティには現在フォーカスを持っている(アクティブな)ノードが格納されていますが, このノードが更にシャドウツリーを持っていた場合は, ここから更にShadowRoot.activeElementプロパティをたどることでシャドウツリー内部でのアクティブなノードを取得できます.

指定した座標でシャドウツリー内を検索する

Document.elementFromPointメソッドを用いると, 指定した(ビュー範囲の左上から計った)位置に見えているノードを抽出できます. ここでシャドウツリー内部に存在するノードまで詳しく調査したい場合はShadowRootオブジェクトの同名のメソッドを利用します.

シャドウツリー内で選択された範囲を取得する

Window.getSelectionメソッドを用いると現在選択中の内容を取得できますが, シャドウツリー内部の選択範囲を取得するにはShadowRoot.getSelectionメソッドを使います. しかし現状では仕様も曖昧であり, ブラウザ側の動作も今ひとつ納得出来るものではありません.

シャドウツリーにおけるリンクとid属性

シャドウツリー内にa要素を配置しリンク機能を挿入した場合, そのリンクは一般のa要素と同様に有効となります.

a要素の参照可能範囲

a要素が参照できるリンク先はメインツリー内に配置されている必要があります. つまりシャドウツリー内にリンクのhref属性値と合致するid値の要素が含まれていたとしても, それは見た目だけであり, 実体が無いためリンク対象とはなりません. 逆にcontent要素やshadow要素によってシャドウツリーに投影されている要素は, 実体が存在するためリンクの対象となります.

anchorhref
in→in
(other)
in→outtarget
in→out
(wrap)
target
out→inlink

なお, スクリプトを用いてシャドウツリー内を検索し, 強制的にフォーカスを与えたとしても, 「:target」擬似クラスは有効となりません. (「:focus」擬似クラスは有効です).

SVGのxlink:href属性とシャドウツリー

(インライン)SVGではHTMLでのhref属性と同様にxlink:href属性が様々な役割を担っています. 例えばuse要素ではこの属性が参照している図形ノードを複写することが出来ます. しかし先ほどのリンクの例とは異なり, use要素によるノードの参照はツリー内を直接検索するため, 参照先が同一ツリー内に存在しないと正しく動作しません.

main→shadow

これらのことからシャドウツリーにおけるid属性の扱いは少なくとも単一のシャドウツリー内で一意の値が取れれば十分ということになります.

シャドウツリーとタブ順

シャドウツリーを用いたWEBページにおけるタブ順は, 原則的にメインツリーとシャドウツリーの順序がミックスされ, 見た目に沿った自然な順序となります.

フォーム機構とシャドウツリー

ここまででスクリーンに描画される内容がシャドウツリーに置き換わることが判りました. では, フォーム部品をシャドウツリーで上書きしたり, 挿入したらどのような結果となるでしょうか?

結論から言うと, フォーム部品の機能にシャドウツリーの内容は影響しません. つまり, フォーム部品の見た目をシャドウツリーでオーバーライドしても, 基本となるフォーム部品の機能は失われず, データ送信の対象となります. また, シャドウツリー内部にフォーム部品を挿入しても, シャドウツリーの外側のform要素からその内容を確認出来ませんからデータ送信の対象とはなりません.

シャドウツリーを使ったフォーム機能の注入

form要素のフォーム送信が有効となるには「form要素」と「submitボタン」が同じツリー上に存在している必要があります. 次の例ではform要素がメインツリーに, サブミットボタンがシャドウツリー上にあるため, 見た目上は動作しそうですが上手く行きません.

form要素をシャドウツリー内に移動すると, フォーム送信が有効となります.

フォーム部品とシャドウツリー

input要素の表示内容をシャドウツリーで上書きすると新たなフォーム部品を生成することが可能となります. 次の例ではシャドウツリー内にラジオボタングループを生成しています.

ここでShadow DOMをサポートしない環境やJavaScriptが無効な環境では単なるテキスト入力に見える点に着目しましょう. つまり, 基本的な機能をそのままに, 環境に応じて最適な入力フォームを構築できるのです.

フォーム部品作成時の注意点

とは言え, 実際にフォーム部品を作るとなるとそれなりに考慮すべき点が出てきます. 以下は筆者が気付いた点です.

  • input要素にシャドウツリーを追加すると, input要素標準の動作が全てキャンセルされます.
    つまり, クリックやキー入力による状態変化処理が一切無効となります. 従ってこれらの処理を全て自作する必要があります.
  • シャドウツリー内からは元となるinput要素の状態を:host()擬似クラスセレクタで取得可能です.
    従って, これを::shadow擬似要素セレクタと組み合わせることでシャドウツリー内部のスタイルを変更することが可能です.
  • コントロールの見た目をCSSのappearanceプロパティを用いて変更可能とします.
    但し, このプロパティは標準的なものではありません.
  • シャドウツリー内に新たなinput要素を設ける場合は, 最終的に親となるinput要素の状態を書き換えます.

以上を踏まえた上で, チェックボックスをオーバーライドしてみましょう.

他の部品についても, 多少の処理の追加が必要となるでしょうが, 概ね同じような形で拡張することができるでしょう.

フォーム部品上書きのアンチパターン

フォーム部品の内部を不用意に書き換えてしまうと非常に厄介な問題を引き起こすことがあります. 次の例ではわざと見た目のvalue値と本来取得できるvalue値が異なると言った混乱を引き起こしています.

Shadow DOMをSVGに応用する

SVGはHTML内にインライン形式で展開することが可能です. 従ってShadow DOMと組み合わせて利用することが可能です.

SVGをシャドウツリーに挿入する

一般にSVGはHTMLと同様に複数の要素から構成されており, これをHTML文書にそのまま埋め込んで利用することが可能です. これをインラインSVGと呼びます. しかしインラインSVGには, HTML文書とSVGとをマージすることでid値が重複する危険性を孕んでいます. SVGでは仕組み上id値が文書中に散りばめられる傾向があるため, 中々対処しにくい面があります.

一方でobject要素やimg要素からSVGグラフィックを読み込んだ場合はこのような問題は発生しません. しかし, フレーム外からのスタイル適用や, スクリプトによる操作が難しくなります.

この時, シャドウツリーにSVG構造を挿入すると, 単一コンテキストで動作しつつ, id範囲が分離されることでこれらの問題がほぼ解決します. シャドウツリーの内容はCSSやスクリプトから自由にアクセスできるからです. 但しシャドウツリーの内部からは外部のノードを参照することが出来ない点に注意しましょう.

SVGElementに対するシャドウツリーの生成

createShadowRootメソッドはElementオブジェクトで定義されています. 従ってHTMLのみならずSVGにおいても呼び出すことが出来ます.

とは言え, HTMLとSVGとでは内部構造が大分異なります. 従って理屈上はAPIが備わっているとしても, 仕様が固まっていない状況では使うことが出来ません. また, Chromeでは現状エラーとなるようです. Chrome40から動作するようです.

Custom Elementsとは

HTMLは本質的に文書を構造化するためのものです. 従ってHTMLが提供するタグはそれぞれ文書構造における汎用の役割を担っているだけで, 使い方によってはより具体的な意味付けが欲しくなります. そこでHTML文書に独自のマークアップを追加し, スクリプトにより固有の振舞いを与えると言ったアイディア・(広義の)カスタム要素が生まれました.

このカスタム要素を実現する手法としてはこれまでも様々な方法(jQuery UI等)が提唱されていました. しかし考え方の相違からライブラリ間の相性問題が発生しやすく, 必ずしも使い勝手の良いものではありませんでした. Custom Elementsはこの問題を解決するため, カスタム要素を定義するための標準的な仕組みを提供します. また作成したカスタム要素は他のHTML要素と同等に扱われるため, 独自ノードの生成・複製・挿入・破棄と言った操作の全てをWEBブラウザ側に任せることができます.

例えば次のようなHTMLがあったとしましょう. これまでは, カスタム要素my-header, my-tabs...に対して外側から見た目や振舞いを与えることで, いわば操り人形のように操作していました. Custom Elementsを導入することで, これらが(Custom Elements"形式"の指示書に従い)能動的に動作するようになります. これは要素レベルでの部品化(機能の分離)が可能という点で, とりわけWEBアプリケーションの構築時に恩恵をもたらします.

簡単な例を示します. ここでは「my-img」要素を定義して画像を表示しています.

この時, スクリプトがDOM内の「my-img」要素を一切操作していない点に注意してください. これは「my-img」要素の定義を検知したWEBブラウザが, DOM内のカスタム要素を適切に処理した事を示しています.

以下, カスタム要素はCustom Elements仕様に準拠したカスタム要素(狭義のカスタム要素)を指すものとします.

カスタム要素の導入ケース

カスタム要素は次のようなケースにおいて利用できます.

  • 独自の要素をHTML文書にマークアップするだけで処理を行わせる.
    カスタム要素をスクリプトの実行トリガとして利用できます.
  • 既存のHTML要素を拡張(継承)し, 新たな要素を作り出す.
    煩雑な属性値の設定をカスタム要素の初期化処理時に行わせることでHTMLコードをスリム化します. また, 既存ノードを役割毎に細分化できます.
  • 複数の要素を論理的に一つにまとめる.
    ノード間の親子関係をカスタム要素としてひと括りにし, 親要素の生成時に子要素を一括生成する事が出来ます. これはHTMLパーサーによる不足ノードの自動補完処理に対応します.

カスタム要素定義に関わる留意点

このように新たな要素を定義することがCustom Elementsの導入意義ですが, その自由度故に使う側できちんとした設計の責任を負う必要が出てきます. 要素の命名や役割分担等を意識的に行わないと, 途端に運用が困難になってしまうでしょう. とりわけCustom Elementsではオブジェクト指向的な考え方が強制されるため, 慣れない内は敷居の高い機能でもあります.

また, 効果的に利用するには様々な課題があります. 下記に代表的なものについて考えていますが, いずれもカスタム要素の利用者の視点に立っている処に注意してください.

HTML仕様を基準に考える

カスタム要素といえど, 完全に一から作るべき要素といったものは滅多にありません. 既存のHTML仕様と照らし合わせ, 目的や役割に合致するものがあればそれをベースに機能を拡張すべきです. その一方でカスタム要素としての機能が無効なケースにおいても文脈が破綻していない事を確認します. 例えばtime要素であれば, いかなる場合においても日付・時刻情報を指し示すことが求められます.

既存の動きを参考とする

これから実装しようとしている仕組みにアイディアの元となる物があるのであれば, 出来る限り操作感を統一すべきです(例えば, ボタンであればいかにも押せそうな見た目をしている等です). これはWeb環境に留まらず, 一般的なアプリケーションGUIの動作や, 日常手にする電化製品の動作, より普遍的な物理法則と言ったものを参考とするのも効果的です. こうすることで実際にそのカスタム要素を操作するエンドユーザーの使い勝手(ユーザビリティー)が向上します.

カスタム要素を多用しない

WEBブラウザは必ずしもあなたが作ったカスタム要素の動作に最適化されているわけではありません. 一度に多量のカスタム要素を使った場合, ネイティブコードよりも負荷の高いスクリプトコードが多量に動作することで動作パフォーマンスを損ないます.

role属性を使ってカスタム要素の役割を宣言する

通常, 自作したカスタム要素は客観的に何を表すものか判りません. そのため, (JavaScriptが創りだした)見た目の情報が存在しない, ソースコードの世界やスクリーンリーダーの環境においてはその内容を正しく伝達することができません. そのため, アクセシビリティを重視する場合はrole属性やWAI-ARIAを使って, これがどのような役割を担っているか, どのような状態にあるかを宣言しましょう.

補足)カスタム要素定義の方法に係る議論

現在のCustom Elements仕様では伝統的な「prototypeチェーン」によるオブジェクト継承によってカスタム要素を定義することを想定しています. しかし並行して仕様検討が成されているECMAScript6(JavaScript実装に対する次期標準仕様)では, 新たに「class/extends宣言を使った継承」機構が追加される予定です. このことからCustom Elements仕様はECMAScript6仕様と整合性がとれていないとの意見があります.

Custom Elements仕様においても決してECMAScript6を蔑ろにしている訳ではなく, ECMAScript仕様の確定後に改めて仕様の詳細化をするとのことですが, その結果, 先行実装している側(Chrome/FireFox)とそうでない側(Safari)との間に温度差が生じています.

カスタム要素の定義の流れ

カスタム要素の定義・生成は概ね次の流れに沿って行われます. 一見複雑に見えますが, 本質的に通常JavaScriptで行われているオブジェクトの継承手順と全く同じです.

  1. カスタム要素の名称と継承元(HTMLElement/SVGElement)を決定します.
  2. Object.createメソッドを用いてカスタム要素のプロトタイプを生成します.
  3. プロトタイプにライフサイクルコールバック関数やプロパティを登録することで, カスタム要素としての振舞いを定義します.
  4. document.registerElementメソッドを使ってカスタム要素をDOMに登録し, コンストラクタを取得します.
  5. コンストラクタを使ってカスタム要素オブジェクトを生成します.
  6. カスタム要素オブジェクトをDOMに挿入します.

先程の例と照らしあわせてみましょう.

カスタム要素の命名規則

カスタム要素に付ける名称は次の条件を満たす必要があります.

  • カスタム要素として一意であること
  • NCNameの条件を充たすこと
    ※数字・アンダースコア「_」で始まっておらず, 名前空間を区別するためのコロン「:」を含まなければ大抵は問題ありません. 理論上は日本語を使うことも出来ますが, おすすめしません.
  • 一文字以上のハイフン「-」を含むこと
    これはHTMLやXMLをベースとする言語で既に使われている要素名との名前の衝突を避けるための条件です.
  • アルファベットは全て小文字であること
    HTMLはタグの大文字小文字を区別しませんが, 要素名を登録する場合は小文字を用います.

但し, 次の名称は既にSVG/MathMLで定義されているため使用できません.

annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph

カスタム要素の命名ルール

カスタム要素はその性質上HTML文書の様々な部分に埋め込まれて使われます. 従って他の要素と同様にタグ名を見ただけでその役割が判るように命名すべきです. つまり「my-element-001」のような一見して何を表しているか判らないものよりも「my-maildata-container」と言ったように中身の内容が判るものにしましょう. また, 全体としての命名ルールを設けるのも効果的です.

また, 将来的にカスタム要素をパッケージ化し公開することを考えているのであれば, 他のカスタム要素と名称が衝突しないように工夫する必要があります. しかしその標準的な手法についてはまだ議論されていません.

カスタム要素のプロトタイプを生成する

WEBブラウザに表示されているオブジェクトは全てElementオブジェクトを継承しており, 全体としてDOMツリーと呼ばれる木構造を形成しています. カスタム要素はこのDOMツリーに挿入されますから, 少なくともこのElementオブジェクトを継承する必要があります. 次の図はオブジェクトの継承関係を示しています.

カスタム要素はこの右側のツリーのどのオブジェクトをも継承することが出来ます.

継承元のオブジェクトを生成する

JavaScriptにおける継承は, プロトタイプチェーンと呼ばれる仕組みを使って実現します. つまり継承元オブジェクトのインスタンスを新たなオブジェクト定義のプロトタイプとして扱うことで(擬似的に)継承を表現します.

さて, カスタム要素を既存の要素を拡張するのではなく一から定義する場合, 通常HTMLElementオブジェクトを継承するようにします. しかし, HTMLElementには直接的なインスタンスの生成機構(コンストラクタ)を持たないので, 汎用のオブジェクト生成API(Object.create)を使う必要があります.

Object.create(prototype, properties)
指定したプロトタイプから新たなオブジェクトを生成します. propertiesには追加したいプロパティの設定情報を指定します.

補足)継承元オブジェクトの選択

この時作りたい要素が文脈上既存のHTML要素を継承しているとしたほうが自然な場合は, 極力その要素を継承してください. これは実際のHTMLコードをイメージすれば実に当たり前のことです. 例えば, ol要素配下においてli要素のように振る舞うカスタム要素を定義するとします. この場合,

と記述するよりも, としたほうがよりカスタム要素の役割が判りやすくなります. また, SVGに属するカスタム要素を定義する場合は, 既存のSVG要素からカスタム要素を派生させる必要があります. 詳しくは後述します.

カスタム要素の振舞いを定義する

上記で得られたプロトタイプオブジェクトには, カスタム要素として振る舞うための処理を記述していきます.

ライフサイクルコールバック関数を定義する

プロトタイプオブジェクトに特定のプロパティに関数を登録しておくと, カスタム要素に対する操作を行った際にDOMツリーから自動的に呼び出されるようになります. これをライフサイクルコールバック関数と呼び, 次の4種類が存在します.

  • createdCallback()
    カスタム要素のインスタンスが生成された時, もしくはDOMに存在しているカスタム要素候補にカスタム要素定義が結びついた際に実行されます.
  • attachedCallback()
    カスタム要素が(現在表示中の)documentオブジェクトに挿入された際に実行されます. DOMの構造を意識する必要がある場合に便利です.
    ※「現在表示中」とは直接ドキュメントを開いている場合や, iframe要素等からドキュメントを表示している場合を指します.
  • detachedCallback()
    カスタム要素が(現在表示中の)documentオブジェクトから離れた際に実行されます.
    廃棄すべきデータの
  • attributeChangedCallback(attrName, oldValue, newValue, namespace)
    カスタム要素に対する属性値に追加・変更・削除があった際に実行されます. 引数の内容でどのような変更があったのかが判定できます.
    状態attrNameoldValuenewValuenamespace
    追加null
    変更
    削除null

    なお, 属性値を同じ値で上書きした場合(つまりsetAttribute("xxx", getAttribute("xxx")))にattributeChangedCallbackは呼び出されません.

例1) 親要素の種類によって表示形式を変えるカスタム要素
my-items要素はカンマ区切りのテキストをli要素に変換して表示します.

例2) 扇型を表示するカスタム要素
my-pie要素は扇型を描きます. 開始角(start), 終了角(end)を指定します.

カスタム要素オブジェクトにプロパティを定義する

先ほどの例でも判るように, Object.defineProperties/Object.definePropertyメソッドでプロトタイプオブジェクトにプロパティを定義することで, カスタム要素オブジェクトから呼び出すことが可能になります.

Object.defineProperty(obj, prop, descripter)
指定したオブジェクトにプロパティを定義します.
Object.defineProperties(obj, properties)
指定したオブジェクトにプロパティを一括で定義します.

カスタム要素を登録する

プロトタイプオブジェクトを構成したらdocument.registerElementでカスタム要素を登録します.

Function:Document.registerElement(type, options)
指定した名称のカスタム要素を当該windowコンテキストにおけるカスタム要素レジストリに登録し, カスタム要素オブジェクトを生成するためのコンストラクタを返します. 既に登録済みの名称を渡した場合, エラーとなります. これは本メソッド実行時にDOMツリー内のカスタム要素候補との紐付処理が為されるためで, この紐付をやり直すことが出来ないからです. 同様にメソッドの実行を取り消すことは出来ません.

引数option(ElementRegistrationOptions)には作成したプロトタイプオブジェクトの他にも様々な設定を記述することが出来ます. 以下にその内容について示します.

object:ElementRegistrationOptions.prototype
カスタム要素のプロトタイプオブジェクトを指定します.
DOMString:ElementRegistrationOptions.extends
既存の要素を継承する際の継承先タグ名を指定します(継承時は必須). 継承元の要素がDOM上ではHTMLElementに割り当てられていると, プロトタイプオブジェクトの内容だけではカスタム要素の適用先が判らなくなるため, 適用先を明示する必要があります.

カスタム要素の中身をそっくり取り替える

このようにカスタム要素の定義はスクリプトから行います. そのため, 実行環境の状況によって割り当てるプロトタイプオブジェクトを交換することで, 同じマークアップに対して異なるカスタム要素機能を挿入することができます.

例えば開発環境ではデバッグのための構造を仕込んでおき, 運用環境では動作が軽快となるようにシンプルなものとする等の応用が考えられます.

カスタム要素の一括指定

上記をまとめると, カスタム要素の定義を逐次行うのではなく, 一括して行うことが可能です. 先程の例は次のように書き換えることが出来ます.

カスタム要素の有効範囲

カスタム要素は当該windowコンテキストの範囲でのみ有効です. また当たり前ながら, HTMLを文字列にシリアライズした場合についてもカスタム要素としての振舞いが欠落します.

補足)フレームを介したカスタム要素の授受

カスタム要素がwindowコンテキスト毎に管理されることは既に見ました. 従って, iframe要素や別ウインドウに開いた文書とノードの授受を行う場合, 内部実装の異なる同名のカスタム要素が混在できることになります. この時, Element.cloneNodeメソッドを使ってノードをコピーした場合, そのノードが所属するコンテキストにおいてカスタム要素が初期化されるため, 内部実装に不整合が発生します. この場合はDocument.importNodeメソッドを用いると挿入先のdocumentにおけるカスタム要素として動作するようになります.

カスタム要素オブジェクトのコンストラクタを活用する

document.registerElementメソッドの戻り値はカスタム要素オブジェクトを生成するコンストラクタです. 一般にElementオブジェクトはdocument.creaetElement(もしくはdocument.createElementNSメソッド)を使って生成しますが, コンストラクタを使うとこの長い記述をせずに済みます.

カスタム要素のライブラリ化

生成したコンストラクタをそのまま使っても良いですが, 用途や役割毎にまとめておくとライブラリとして活用しやすくなります.

カスタム要素を更に継承する

自作したカスタム要素をベースとし, 新たなカスタム要素を生成することが出来ます. プロトタイプチェーンを辿り, 継承元のメソッドをオーバーライドすることで機能を追加します. この場合, 継承元で作成されたシャドウツリーを装飾する場合はshadow要素を用いるとよいでしょう.

HTMLのパーシングとカスタム要素

カスタム要素は既存のHTML要素と同様に予め文書中に記述しておくことが出来ます. しかしカスタム要素の定義はスクリプトによって行われるので, 定義処理の前にDOMツリー内にカスタム要素(の候補)が存在している可能性があります. そのため, カスタム要素が正しく解釈されるために特殊なHTMLパーシング(HTMLのコードからDOMツリーを構成する処理)が行われます.

タグ名によるインスタンス化の振り分け

HTMLのパーシング処理では, 通常解釈出来ないタグ名の要素が見つかった場合, HTMLUnknownElementオブジェクト(つまり不明な要素)としてDOMツリーに挿入します. しかしCustom Elementsによる機能拡張により, タグ名にハイフンが含まれている「カスタム要素になりうる要素(これを未解決の要素と呼びます)」についてはHTMLElementオブジェクトとして扱います.

カスタム要素機能の注入

document.registerElementメソッドが実行されると, DOMツリー内から未解決の要素のうちタグ名と引き数のカスタム要素名とが一致するものが抽出され, 初期化処理(ライフサイクルコールバック関数の呼び出し)が行われます. その際createdCallback→attachedCallbackの順で処理が呼び出されます. この処理は同期的に行われます.

ツリー構造とカスタム要素の初期化順

registerElementで登録したカスタム要素が文書中に複数存在した場合, DOMツリーにおける自然なノード順(≒HTMLコード順)にコールバック関数が呼び出されます. 次の例では同名のカスタム要素が入れ子となっていますが, この場合は親ノードから子ノードの順に初期化されます.

また, 複数種類のカスタム要素が親子関係を持つ場合, カスタム要素の登録順でコードの成否が変化することがあります. 次の例では子要素から親要素の内容を参照しているため, 初期化前の親要素を参照しているか否かで動作が変化します.

この問題を回避するために, 予めどのような順にカスタム要素を定義すべきかについて検討しておきましょう. またカスタム要素の定義順に依らない設計としておくことも大切です.

:unresolved擬似クラスとFOUCの回避

未解決の要素にカスタム要素としての機能が注入されるまでの間, スクリプト処理によって再レイアウト処理や再描画処理が発生します. そのため複雑なノード操作をする際は, この必要のない動き「=ちらつき(FOUC:Flash of unstyled content)」を回避する必要が出てきます.

ちらつきの原因には次の二つが考えられます.

  • カスタム要素のサイズが変化することに依る再レイアウト
  • カスタム要素の内容が変更されることに依る再描画

前者については予めカスタム要素に固定的なサイズを指定しておくことで解決します. 後者については次に示す:unresolved擬似クラスセレクタを利用します.

:unresolved擬似クラスセレクタ

:unresolved擬似クラスセレクタはCustom Elementsで定義されたもので, 未解決のカスタム要素(つまり「xxx-xxx」形式の未知の要素)にマッチします.

カスタム要素の解決と:unresolve擬似クラス
:unresolved
未解決のカスタム要素候補にマッチします

解決済みのカスタム要素 未解決のカスタム要素候補 不明な要素

この:unresolved擬似クラスに対して「visibility:hidden」や「opacity:0」を指定しておくことで, カスタム要素登録中のちらつきを隠すことができます. なお「display:none」を指定した場合はページの再レイアウト処理を誘発するため避けたほうが無難です.

カスタム要素とエラーノードの再配置

但し, カスタム要素もHTMLのパーシングルール上は不明なノードに該当します. 従ってtable要素のように内部に記述可能なノードの種類に制限があった場合, DOMツリーへのパーシング時にエラーノードの再配置によって意図した位置にノードが挿入されないと言う現象が発生します.

これらの問題は, 既存の要素を継承することで対処可能です.

SVGとカスタム要素

SVGではDOM中に現れた不明なノードをスクリーン描画処理上常に無視することになっています. この動作との整合性を鑑み, SVGにおいてカスタム要素を定義するには既存のSVG要素を継承することとされています. Custom Elementsのサポート有無で大きく見た目が変化してしまうのは望ましく無いからです.

既存の要素を継承する

カスタム要素を定義する方法にはもうひとつ, 既存の要素を継承する方法があります. 既存の要素を継承することで, HTML上の文脈やパーシングルールを維持しながらカスタム要素としての機能を追加することができます. 継承の仕方はこれまでとほとんど変わりませんが, 2箇所変更すべき点があります.

  • プロトタイプオブジェクトを生成する際に, 継承したい要素オブジェクトのprototypeを用います.
  • registerElementメソッドの第二引き数のextendsプロパティに継承した要素のタグ名を追加します.

次はbutton要素を拡張する例です.

同様にSVGに属する要素を拡張することができます.

専用のインターフェースを持たない要素を拡張する

HTMLの要素の中にはHTMLDOM内に専用のインターフェースが用意されていないものがあります. 例えばsection, article, nav等の要素はDOMツリー内ではHTMLElementオブジェクトとして展開されます. 従ってプロトタイプオブジェクトの生成にはHTMLElement.prototypeを用います.

既存の要素を継承するための拡張仕様

既存要素を継承した場合, 元のタグ名と異なるタグ名を付けてしまうと先ほどのHTMLのパーシング処理で問題を引き起こします. そのため, この要素がユーザーによって継承されていることを明示するための属性が追加されています.

is属性
既存要素を拡張した際のカスタム要素名を指定します. この内容を型拡張記述と呼びます. HTML/SVG仕様で定義されている要素名のみが対象です.
先程の例では<button is="my-button" …と記述することで, 「このbutton要素はmy-button要素として扱う」ことをDOM側に明示しています. 要素のカテゴライズを行う観点からclass属性によく似ていますが, その役割からis属性には単一の値しか記述できません.

また, is属性の追加に伴い, Elementオブジェクトを生成する幾つかのメソッドに引き数が追加されます. これらはdocument.registerElementメソッドで生成したカスタム要素オブジェクトのコンストラクタを使えない場合に利用します.

Document.createElement(localname, typeextention)
指定した要素を生成します. typeextentionにはlocalname要素を継承しているカスタム要素の名称を指定します.
Document.createElementNS(namespace, localname, typeextention)
指定した名前空間の要素を生成します. typeextentionにはlocalname要素を継承しているカスタム要素の名称を指定します.

is属性の扱い

is属性はElementオブジェクトの生成時に一度だけ読み込まれます. この時のis属性の内容によって, 既存のHTML要素か, カスタム要素候補か, カスタム要素かの判定が為されます. 従って後からis属性に内容を設定・変更したとしても, それは既存要素を継承したカスタム要素とは解釈されませんし, 別の型のカスタム要素とは解釈されることもありません. 従って, is属性は属性でありながらタグと同様に後から変更することはできません.

継承と型判定

一般的に行われているinstanceof演算子やtypeof演算子による型判定は, カスタム要素でも可能です. なお, 利便性を鑑みるとtoStringメソッドをオーバーライドしておくと後々扱いやすくなるかも知れません.

未解決)未定義の要素を独自実装する

今後HTML仕様に要素が追加された場合, その内容を先取りしてカスタム要素で実現する場合を考えます. すると, 新たな要素をHTMLに記述しておき, その要素をカスタム要素機構で継承する形を採ることで, 理論上将来ブラウザがその要素をサポートした際に何も変更すること無く使い続けることが可能です.

しかし, 現状FireFoxではextendsプロパティに既存のタグ名のみ指定可能であり, 具体的なフローについてはよく判っていません.

カスタム要素へのスタイリング

カスタム要素のスタイルは継承元の要素のスタイルを引き継ぎます. なお, HTMLElementを継承した場合は, スタイルが未指定となります. (シャドウツリーを含む)配下に要素が無ければ全く表示されませんが, 適切にスタイルを指定することで他の要素と同じように扱うことも可能です.

属性セレクタを使ったカスタム要素判定

既存の要素を継承している場合は, 属性セレクタ「[is=[カスタム要素名]]」を使ってカスタム要素を選別することが可能です.

カスタム要素とカスタムイベント

カスタム要素を定義する上で, カスタムイベント機構は非常に相性の良い仕組みです.

カスタムイベントを内包する

カスタムイベントはユーザーが自由に定義できるイベントです. 既存の(click, mouseover等の)イベントでは(処理上は十分なものの)具体的に何を通知したいのかが判りにくい場合があります. カスタムイベントではこの名称や通知するデータについての裁量が使う側に委ねられるため, 柔軟なイベント設計が可能となります. 更にカスタム要素と組み合わせると, このカスタムイベントの定義・着火機構を内包させることが出来るため, 全体としての使い勝手が向上します.

CustomEvent(type, eventDict)
カスタムイベントオブジェクトを生成するコンストラクタです. typeにはイベントの名称を, eventDictにはイベントの特性やリスナー側に通知したい内容を設定します.
detail
イベントのリスナー側に通知したい内容を設定します.
canBubble
イベントをDOMツリーの上位ノードに通知(バブルアップ)するかどうかの真偽値です.
cancelable
イベントをキャンセルできるかの真偽値です.
Element.dispatchEvent(event)
イベントを着火し, 登録済みのリスナー関数の実行を促します.

下の例では定期的にticイベントを発生するmy-timer要素を定義しています.

カスタムコントロールとカスタムイベント

このようにカスタムイベントによる通知処理はイベントそのものに固有の名称をつけられるため, シャドウツリーを用いて複数のフォーム部品を一つにまとめる(つまりカスタムコントロールを構成する)場合に効果的です.

Web Componentsでプラグインを構成する

最後のまとめとして, Web Componentsの仕組みを総動員して簡単なプラグインを作ります. 全ての仕様が過不足無く連携していることが判るでしょう. 更に, 元のHTMLではプラグインの導入とカスタム要素の記述だけで済んでいる点に着目してください. そのため, 複数のページで同じライブラリやカスタム要素を使いまわすことが非常に楽に出来るようになります.

具体的な手順

プラグインを構成する場合の手順を見てみましょう.

  1. プラグインを定義するHTMLファイルを用意します. 外部ライブラリを必要とする場合は, ここに記述します.
  2. [Custom Elements]script要素を用意し, カスタム要素を定義するためのスクリプトを記述します.
  3. [Shadow DOM]プロトタイプオブジェクトのcreatedCallbackにシャドウツリーの構築ロジックを挿入します.
  4. [template要素]シャドウツリーに挿入するDOM構造はtemplate要素として定義しておきます. template要素にはスタイル情報を挿入しておきます.
  5. [HTML Imports]プラグイン化したHTMLファイルをlink要素から読み込みます.

プラグイン作成時の検討事項

個人で利用する場合はともかく, 作成したプラグインを一般に公開するのであれば, 既存の環境を汚染しないような対処が必要です. 例えば次のような点に注意します. こうすることでプラグイン間の干渉を未然に防ぎ, より使いやすいプラグインとなります.

  • 極力グローバル変数を使わない
    (windowオブジェクトが管理している)グローバル変数は様々なライブラリやスクリプトが共有しています. 従ってプラグイン側では全機能を単一のnamespaceオブジェクトに詰め込み, 1グローバル変数で全てが賄えるようにします. (先程の例では「MyPlugin」がnamespaceオブジェクトです.) また, この変数名を動的に変更できるようにしておくと, 万が一グローバル変数名が干渉した際にも対処しやすくなります. カスタム要素のタグ名称についても全く同じことが言えます.
  • 特定のライブラリ・バージョンに依存しないように記述する
    作成したプラグインが既存のスクリプトライブラリに依存していた場合, 他のプラグインとの干渉する可能性があります. なぜなら組み合わせたプラグインにおいても, 何らかのライブラリを内部で読み込んでいる可能性があるからです. また同名のライブラリを利用していたとしても, 細かなバージョンの違いから正しく動作しないと言った問題が発生し得ます.
    従ってプラグインを構成する際は原則としてWeb Componentsの標準機能だけを用いるようにし, どうしてもライブラリの導入が必要な場合は, その有効範囲がプラグイン内部のスコープに限定されるように工夫しましょう.
  • プラグイン全体としてのファイル数を減らす
    動作に必須となるデータが個別のファイルとして管理されていた場合, プラグイン毎にその配置を検討せねばならず, 結果として利用者側の管理コストを増大します. よって, プラグインを構成するスタイル・スクリプト等は出来る限り単一のHTMLファイルにまとめるようにしましょう. また, 画像等のデータもURIデータスキーム形式としてファイルに埋め込むことが出来ます.

このようにメイン文書の構造を出来る限りシンプルに保ちつつ外側から多彩な機能を注入できる, それこそがWeb Componentsが目指している未来なのです!