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仕様の拡張となっていることから, 手に馴染んだスクリプトライブラリを手放すこと無く新しい技術の恩恵を受けられるというメリットもあります.
しかしその効果を最大限に享受するには, 暫く時間が掛かります. というのも拡張される機能がHTMLの広範に渡るからです. そのため, 既にあるHTMLの機能との組み合わせの量が計り知れないものとなっており, どのような使い方が効果的なのかについての考察が追いついていません.
WEB部品の構成といったアイディアそのものは古く(2000年代中頃)から存在しており, XBLやSVG1.2等にその名残が見受けられます. しかし, 当時はまだWEB標準化そのものが初期段階にあり, 比較的高水準な仕組みと言えるWEBの部品化機構の実装まで漕ぎ着くことが出来なかったようです.
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を利用するだけでも再現できましたが, 標準仕様としてその手段が提供されることで, コードの再利用性が高まります.
各仕様の粒度が細かいのには意味があります. こうすることで仕様そのものの再利用性が高まると共に, 他の仕様やフレームワーク等と組み合わせることで新たな価値を生み出すことが容易くなります.
また単一の巨大な仕様では全体としての整合性を保つことが難しい上, 内容に瑕疵が見つかった際の仕様修正が膨大となりがちであり, これでは仕様を実装するブラウザ側においても大きな負担となってしまいます.
これが複数の小さな仕様に分割されていれば, それぞれにおいて仕様設計におけるPDCAサイクルを上手く回すことが出来, その結果仕様としての精度を向上することが可能となります. この傾向はHTMLのみならずCSSやSVGと言ったWEB技術全般に言えることです.
補足)宣言的なコンポーネント定義
Web Componentは当初スクリプトを介した手続き的な部品の登録のみならず, HTML上のマークアップによる宣言的なコンポーネント定義をも視野に入れていました. しかし, 後者の仕様化は様々な課題を抱えていることから初期段階での仕様検討が見送られることになりました. 今後, Web Componentsが技術的な成熟を見たタイミングで再度仕様が見直されるかも知れません.
element要素や, decorator要素等がこれにあたります. template要素の内容が自動的にshadow DOMに展開されるといった仕組みを検討していたようです.
Web Componentsとアクセシビリティ
Web Componentsはその仕組みから高機能性が問われるWEBアプリケーション分野での活用を想定しているように見えますが, それだけではありません. 一般にアクセシビリティを意識したWEB設計を行った場合, HTMLの特性から利用できる機能に制限が掛かることはよくあります. しかし, Web Componentsを導入することでアクセシビリティを確保した上で環境に応じたリッチな機能拡張を行うことが可能となるのです.
なお, Web Componentsそのものにはアクセシビリティに関わる仕様が存在しません. 従って, アクセシビリティを確保するのであれば, WAI-ARIAと組み合わせるなどの何らかの作法に則ったWEB部品定義が求められます.
Web Componentsとセキュリティ
Web ComponentsはWEBを取り巻く技術の中でも新しいものです. 従って, その使い方についてのノウハウが全く蓄積されていませんから, 誤った使い方をすることでコンポーネントの再利用性や汎用性を損なったり, 未知のセキュリティホールを開けてしまうことは十分に考えられます. 従って使う側もそのリスクについてはよく理解した上で導入すべきです.
現在Web Componentsを本格的にサポートしているのがChromeに限定されているのも, まずは技術的なノウハウを蓄積するための実験的な色合いが濃いためです. 今後得られた結果が仕様やドキュメントにフィードバックされることで, 他のブラウザでの利用も可能となっていくことでしょう.
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固有のものではないため, 既存のパッケージ管理ツールを導入することである程度解決します.
このように, 新たな開発・管理の環境・体制を整える必要が出てきます. いかなWeb Componentsといえども, 基本的にはHTMLDOMの扱いに対する拡張に過ぎず, これまで行っていたことが, 今後多少やりやすくなるというだけですから過度な期待は禁物です. 場合によっては敢えて使わないと言った勇気も必要でしょう.
ではWeb Componentsを学ぶ必要はないか?
いいえ, そんなことはありません. Web Components仕様は机上で決定されたものではなく, 事実上の動作環境と言えるChromeでの実装を元に様々な開発者からのフィードバックを得た言わば生きた仕様であり, その有効性については既に確かめられています. そのため, 今後は好むと好まざるに関わらずWeb Componentsに触れる機会は増えていく事でしょう. 従って, 現状細かい部分はともかく, 全体としてどのような構成となっているのか程度は理解しておくべきです.
どちらかというと, Web Componentsはアプリケーションベンダ向けの仕組みと言え, 一般的な小規模WEBデベロッパにおいてはこれまでJavaScriptとしての提供が主であったWEB部品が, 今後Web Componentsに準拠したものに置き換わっていくことが考えられます. また今後WEB部品の販売や頒布を計画しているのであれば, Web Components仕様に則った設計としておくことは良い選択です. この場合, Web Components仕様を熟知しておく他にも外部ライブラリへの依存性を排除したり, ブラウザ互換性を考慮するなどして, 使いやすいWEB部品とすることが求められます.
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の観点から見直される可能性があります.
FireFoxでは歴史的な経緯からuse要素配下に架空のツリーが展開される等, Shadow DOMに酷似しています. (余談ですが, この動作は厳密にはSVG1.1に準拠していません. )
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のサポート状況です.
ブラウザバージョンごとの詳細なサポート状況については下記にて調べられます.
† いわゆる"古い"OperaではPrestoレンダリングエンジンを搭載していました. なおこの表では省いています.
この内Chromeでの実装が最も進んでおり, 開発ツール上での配慮もあります. とは言え, キーボードでの操作が考慮から漏れているなど, 導入するには対象OSや利用環境の制限, 仕様不備に依る不具合の許容等の割り切りが必要です.
FireFox環境での設定
FireFoxでは現状Web Componentsの実装は一部に留まっており, 全ての機能を利用するにはフラグ設定が必要です. そのため, 動作検証などの用途にしか使えません. なお, 正式にWeb Componentsのサポートが宣言されたため, 近い将来ほぼ全ての機能が利用可能となるでしょう.
(1)URLバーに「about:config」と入力し, 詳細設定画面を開きます.
(2)フラグ「dom.webcomponents.enabled」をの値を「true」に変更します.
なおpolyfillライブラリを導入しているページを閲覧した際に, Web Components機能が実装済みとして扱われます. しかし既存の実装は不十分なので正しい動作となりません.
それ以外の環境
Internet ExplorerやSafariにおいても仕様の調査に入っており, Web Componentsの未来は明るいとの見解もありますが, 筆者はそうは感じません. 正直な処, 現状の仕様は隙だらけで到底安定運用に値しないからです. これら二つのブラウザはOSにバンドルされているもので, 有償サポートが行われていることから機能拡張については極めて保守的です. つまり, ある程度の設計品質が認められない限り, Web Componentsのサポートは一切為されないものとして考えてよいでしょう.
然るに, かつてのSVGやWebGLがそうであったように, その有効性が確かめられ, 一旦採用すると決まれば間髪を入れずに使えるようになるはずです. 従って今は仕様の品質改善を待つより他にないのです.
モジュール毎にサポート状況を確認する
Web Componentsのモジュール毎にサポート状況を確認するには次のコードを実行します.
サポートライブラリ
Web Components仕様に関わるスクリプトライブラリは既に存在しています. 以下に代表的なものを示します.
本ページではこれらのライブラリの機能・動作についての解説はしません. また本ページに記述されているスクリプトの全てがpolyfill環境においても動作する訳ではありません. なお他のライブラリと干渉することで問題を複雑化することもあるので, そのようなリスクを十分に理解した上で導入してください. 既にWEB上ではライブラリに関わる多くのドキュメント・導入事例が見つかることから, それらを参考とするのもよいでしょう.
polyfillライブラリ
先ほど言及したようにWeb Componentsの機能はいずれも既存のAPIとJavaScriptで概ね実現出来るものばかりです. そのため, Web Componentsをサポートしない環境でも擬似的に動作させるためのJavaScriptライブラリ(polyfill)が提供されています.
webcomponentsjs
Web Componentsを構成する機能を提供するライブラリで, かつてはplatform.jsと呼ばれていました. ネイティブ実装が存在するものはそのままに, Web Componentsとして足りない機能をスクリプトで補完します. なお, 必要な機能毎にライブラリを導入することも出来ます.
Web Components仕様を拡張する
下に挙げるライブラリにおいては, Web Components機構の上に独自のフレームワークを構成することで, 煩雑な手順を踏むこと無くWEB部品を定義できます. Web Componentsをサポートしない環境では上記のpolyfillライブラリと併用します.
この他にも多くのフレームワークがWeb Componentsのサポートを宣言しており, 今後様々な視点によるWEB部品の構成が可能となっていくことでしょう.
情報の入手先
Web Componentsは比較的新しい技術であることから, 信頼に値する文献に乏しいのが現状です. 従って最新の動向を探るためにも, なるべく公式に近いものを参照しましょう. (この文書も疑うくらいが調度よいでしょう. )
現在Web Componentsの紹介と称する記事には, Web Comopnents仕様に関わるものと, より広範なWEB部品の構成(つまりweb components)に関わるものが混在しています. 後者はWeb Components仕様のみならずPolymer等の外部ライブラリを使ったものも含まれており, 意識しないで読み進めると混乱します. あくまでPolymerはWeb Componentsに対する"jQuery"であり, 標準化された仕様に比べてはるかに流動的な内容であることに注意しましょう.
Web Componentsを学ぶ
Web Componentsは決して難しい概念ではありませんが, 実際の動作をイメージしにくいという欠点を持ちます. 従って座学に留めるのではなく, 実際のコードを自分で試してみましょう. ライブラリなどに頼らずとも高々数十行程度のHTMLを記述するだけで, 理解の度合いが変わります. また筆者の経験上, 本記事のようにtemplate要素から始めてCustom Elementsと読み進めていくと, 仕様が本来目指しているゴールがイメージしやすいかと思います.
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やスクリプト等の関連する設定を単一のファイルにまとめられます.
† 正確には「ブラウジングコンテキスト(browsing context) 」を持つ文書と呼びます.
JavaScriptにおける実行コンテキスト(文脈)はスクリプトの実行環境を指します. WEBブラウザにおいては, DOMへのアクセスが可能なwindow環境の他にバックグラウンド処理に特化したworker環境の2種類が存在し, それぞれ利用可能なAPIセットが異なります.
例えば次のようなHTML文書を作っておけば, それを利用する側ではhead要素にlink要素を1行付け加えるだけで済むようになります.
このようにHTML Importsを用いると, これまでは難しかった機能セットの観点からのコード集約が可能となるのです.
よく見るとこのHTMLファイルにはhtml要素が存在しません が, これはHTML5から可能となった記法で, インポートする内容のみを記述できるためHTML Importsに適しています. なおブラウザ環境や記述の内容によってはコンソール上でエラーとなることもあります.
サブ文書をインポートしたlink要素の削除とスタイル
ではスクリプトを用いてlink要素をhead要素から削除したらどうなるでしょうか? この場合, サブ文書に含まれていたスタイル情報は削除されます. しかし既に動作済みのスクリプト処理はそのままとなります. つまりstyle要素やscript要素を個別に削除したケースと全く同じ結果 となります.
インポート可能な文書の種類
HTML Importsでインポート可能な文書はHTML文書に限られます. プレーンXMLやSVG, MathML等のXMLデータはインライン形式で, その他の画像等のデータはBase64形式でエンコードしたデータURIスキーム形式に変換し, HTML文書に埋め込むことでインポートします.
サブ文書としてXHTML形式を選択することも出来なくは無いのですが, 先ほどのような要素の省略記述が出来ません.
サブ文書における外部リソースの参照
link, script, style要素を除き, サブ文書においてimg要素等が外部リソースを参照していたとしても, そのままではリソースは読み込まれません. template要素と同様にメイン文書にそのノードが挿入されてはじめてリソースが読み込まれます.
インポート可能な文書の範囲
XMLデータと同様にインポート可能なHTML文書はメイン文書と同じオリジンにある必要があります. なおこの条件はCORSによって軽減することが可能です.
つまり, WEBフォントやJavaScriptのようにWEB部品を外部に公開できるということです.
インポートの連鎖
link要素が読み込んでいるHTML文書から更に他の文書をインポートすることが可能です. その際, 同一のHTML文書がインポート対象となる場合がありますが, HTML Importsではそのような文書を1度だけ読み込みます. 例えば次のようなインポート関係があったとします.
a.htmのlink要素(linkA)とb.htmのlink要素(linkB)が同じ文書・c.htmを参照しています. すると, 内部的にはlinkAとlinkBとは同じ(c.htmに相当する)documentオブジェクトを参照するようになります(暗黙の共有).
これとは対照的にscript要素は読み込まれる都度, スクリプトを実行します. なぜなら読み込んでいる内容がJSONやAjaxによる動的なコンテンツの可能性があるからです. 従ってスクリプトライブラリなどのような複数回実行が望ましくないケースにおいては, 図のように一度クッションとして専用のHTML文書を定義しておき, スクリプトライブラリをこのHTML文書から読み込むようにします.
単一のHTML文書が複数のlink要素から参照されることがあり得るため, documentオブジェクトからそれをインポートしているlink要素を探すAPIは存在しません. 後述のdocument.currentScript.ownerDocumentとlink.importを突き合わせて自力で検索することはできます.
仕様上はiframe要素とlink要素とで同じHTML文書を参照することができます. しかしiframe要素で開かれた文書は別window配下に置かれるため, iframeが保持するdocumentオブジェクトとlink要素が有するdocumentオブジェクトとは異なるインスタンスとなります.
補足)サブ文書の集約
インポート関係をツリー化出来るとは言え, ドキュメント間に依存関係が存在するとリソースの管理が困難になります. また, 複数のファイルをインポートするのはサーバー負荷やネットワークトラフィックの観点からも避けたいところです. 事前に単一のインポートと出来ないか検討しましょう. コンポーネントの集約を行うVulcanize と言ったツールもあるようです.
補足)HTML Importsの必要性に係る議論
Web Components仕様の中でもHTML Importsそのものは単なるリソースの取り扱いに関わる仕様であり, 既にpolyfillライブラリが存在するなど既存APIのみで(苦労すれば)十分に再現が可能です. そのため, WEBブラウザ側で新たな機能として提供すべきか否かについての議論が存在します.
mozilla(FireFox)ではHTML ImportsよりもECMAScript6で導入が予定されているES6 modules(JavaScriptのコード内からライブラリを呼び出す仕組み)を推進したいようです.
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要素は先行している文書のインポート(つまりその中のスクリプトの実行)が完了するまで実行されません. こうすることで文書リソースに依存性を持つスクリプトの実行順序が保証されています.
script要素におけるdefer属性を思い出してください. これはブラウザにおける(リソースの読み込みやDOM解析を行う)メイン処理を中断してしまうスクリプトの実行を後回しとし, DOMの構築完了直前に(指定した順序で)一括して実行するものでした. リソースが利用可能となるまでスクリプトの実行を待つ点でよく似ています.
この動作を「サブ文書中のscript要素は, あたかもdefer属性が設定されているかのように振る舞う」と説明されることがありますが, これだけでは不十分です. サブ文書内のscript要素にdefer属性が設定されていた場合, サブ文書内でのスクリプト実行順序が後回しとなるだけで, 全体としての実行順は維持されます.
サブ文書内でエラーとなるAPI
上記スクリプトの実行順制御の観点から, サブ文書内のscript要素において次のメソッドを実行しようとするとエラーとなります. メイン文書(document), サブ文書(document.currentScript.ownerDocument)の区別はありません.
document.open()
document.write()
document.close()
もし可能だったとすると, サブ文書におけるスクリプトの実行はメイン文書でのDOM分析処理と非同期的に行われることから, 文書構造が書き換わるタイミングを制御できないことになります.
サブ文書とイベント
サブ文書の読み込みに関わるイベントとしては次の物があります.
link要素のload/errorイベント
サブ文書のDOMContentLoadedイベント
この内, 静的にインポートしているサブ文書のDOMContentLoadedイベントは, メイン文書のDOMContentLoadedイベントの発生直前にサブ文書の読み込み順に一括して発生します. そのため, メイン文書やサブ文書において各種documentオブジェクトのDOMContentLoadedイベントに任意の処理を定義することが出来ます.
サブ文書の読み込み成否をイベントで監視する
link要素はloadイベントとerrorイベントを発生させるため, このイベントに予め処理を登録しておくことでサブ文書の読み込みの成否に対処することが出来ます.
Function:HTMLLinkElement.onload
サブ文書の読み込みが完了した際に実行する関数を設定します. addEventListenerメソッドで処理を登録することも出来ます.
Function:HTMLLinkElement.onerror
サブ文書の読み込みにエラーがあった際に実行する関数を設定します. addEventListenerメソッドで処理を登録することも出来ます.
またloadイベントの特性上, windowのloadイベントが発生したタイミングでは, 全てのサブ文書のロードが完了していることになります.
動的にサブ文書を読み込む
link要素が読み込まれるとサブ文書が利用可能となるまで後続のスクリプト処理がストップします. 従ってサブ文書の読み込みに時間が掛かると, ページ全体としての動作パフォーマンスを著しく損なうことがあります. この問題を避けるにはasync属性(にtrue)を指定しサブ文書を非同期的に読み込むか, スクリプト中でHTMLLinkElementを生成し, ロードタイミングを調整するようにします. この場合, サブ文書の読み込み完了をイベントで捕捉することになります. なお, img要素等と異なり, link要素はhead要素に挿入して初めて効果を発揮します. 従って正しくイベントを発生させるにはDOMに挿入します.
サブ文書を操作するためのAPI
従来のHTMLスクリプティングではメインとなるdocumentオブジェクトは唯一の存在でしたが, HTML Importsの導入によりメイン文書とサブ文書の複数のdocumentオブジェクトを操作する必要があります. そのため, スクリプトを記述する際はどの文書を操作しているのかを意識する必要があります.
スクリプトの実行コンテキスト
先に見た通り, link要素で読み込んだHTML文書はメインとなる文書と同じコンテキストで展開されるため, そこに含まれるスクリプトはメイン文書に埋め込まれているかのように振舞います. そのためwindowやdocument等のグローバル変数はメイン文書でのスクリプトと共有されます.
この動作を利用すると, サブ文書で必要となるJavaScriptライブラリはサブ文書内部で参照すると言ったことも可能です. ですが, オブジェクトの拡張を行うものについては, HTML Importsを意識して記述されているとは限らないため, 正しく動作しない事もあり得ます. また, メイン文書とサブ文書とで読み込むライブラリが干渉すると言った危険性もあります.
スクリプトと所有者ドキュメント
すると, サブ文書内のスクリプトからサブ文書そのものにアクセスする にはどうすればよいのでしょうか? 通常サブ文書はメイン文書の内容を知りませんから, どのlink要素で読み込まれているかを判断することが出来ません. 従ってHTMLLinkElement.import以外の方法を探る必要があります.
この問題はDocument.currentScriptとElement.ownerDocumentを組み合わせることで解決します.
HTMLScriptElement:Document.currentScript
現在のスクリプトが記述されているscript要素を取得します.
Document:Element.ownerDocument
要素が属するドキュメントを取得します.
図に表すと次のようになります.
つまり, サブ文書中のスクリプトが実行されている最中に「document.currentScript.ownerDocument
」を参照することで自身が所属しているサブ文書を参照することが可能となるのです.
イベント処理と所有者ドキュメント
但し, イベント処理等からサブ文書の内容を取得したい場合は, ownerDocumentを何らかの変数に保存しておく必要があります. なぜなら, イベント処理はscript要素で実行されているわけではない ため, 実際のイベント処理時にcurrentScriptプロパティの値がnullとなるからです. なお, 上記の例のようにscript要素直下で変数を定義するとこれはグローバル変数として扱われます. 従ってグローバルスコープの汚染を防ぐ場合は, 一旦無名functionを定義し, その引数としてdocumentオブジェクトを渡すようにします.
ECMAScript6では次のようにブロックスコープを定義することで変数の有効範囲を制限できます.
polyfillライブラリとcurrentScriptプロパティ
HTML Importsをサポートしない環境ではdocument.currentScript.ownerDocumentプロパティは常にwindow.documentオブジェクトに一致します. これはpolyfillライブラリを導入したところで変わりません. 言語仕様上, プロパティ値を上書き定義することが出来ないからです. そのためpolyfillライブラリでは_currentScript
プロパティ等を新たに定義し, HTML Imports環境でのcurrentScriptプロパティの代わりとしています. 従って, polyfillライブラリの有無を考慮するのであれば, 実行中のscript要素を得るには次のコードを用いることになります.
メイン・サブ文書間のノード受け渡し
メイン文書とサブ文書に相当するdocumentオブジェクトは, 単一のコンテキスト(window)を共有することから何れもwindow.Documentインターフェースのインスタンスです. 従ってその間では自由にノードの受け渡しが可能です. つまり, 異なるdocument間でノードの授受を行う際に, document.importNodeメソッドを介さずとも良い のです.
なお, XMLHttpRequestで取得したHTML文書も, 同じコンテキスト上でのdocumentオブジェクトとして扱われます.
一方iframe等の別フレームに読み込んだ文書は, メイン文書と異なるwindow内に展開されるので, 同名のdocumentオブジェクトでありながら, 異なるコンテキスト(window)に属するDocumentインターフェースのインスタンスとなります. 図にすると次のようになります.
蛇足ですが, この場合はメイン文書とフレーム内部とでノードの授受を行う際にDocument.importNodeメソッドを用いてノード内容をコンテキストに応じて再解釈すべきです. なぜなら, DocumentやElementインターフェースに定義されている内容がそれぞれ異なる可能性があるからです. この問題についてはCustom Elementsの項で再度検討します.
サブ文書として開かれている時のみスクリプトを実行する
HTML ImportsはHTML文書をインポート対象とするため, 本来サブ文書として開かれるべきものを直接ブラウザで開くことができます. この時, アクセスログの収集スクリプト等をサブ文書に集約していた場合, 意図しないログが発生することになります.
この問題をクライアントサイドで解決したい場合は, サブ文書の読み込み時の先頭でコンテキストのドキュメントとscript要素の所属ドキュメントとを比較し, 一致した際に後続の処理をキャンセルすることで対応します.
サブ文書にパラメータを渡す
サブ文書におけるdocument.URLプロパティを参照することで, サブ文書のURLを取得できます. 従って, URLに挿入されているクエリ文字列を解析することで, メイン文書からサブ文書にパラメータを渡すことが可能です.
これはカスタム要素等のモジュール登録時にパラメータを要する場合に応用できます. なお, URLクエリ文字列は通常サーバー側に送信されるものですから, WEBサーバーにおけるキャッシュ機構を踏まえて, クエリの内容は固定されたものとすべきです.
HTML Importsと相対パス
サブ文書中に記述された相対パスはサブ文書のパスを基準に解釈されます. 従ってメイン文書に外部リソースを挿入するケースにおいては, リソースへのパスをどのように記述しておくかが問題となります. この方法としては(1)「メイン文書からみた相対パスで記述しておく方法」と(2)「サブ文書から見た相対パスで記述しておく方法」の二つが考えられます.
ここで後者を選択した場合は, メイン文書にパス情報を渡す前に相対パスから絶対パスへの読み替えが必要です.
その方法には様々なものが考えられますが, 最も単純な方法としてはDOMプロパティを利用する方法です. HTMLImageElementのsrcプロパティやHTMLObjectElementのdataプロパティ等, URIを扱うプロパティに相対パスを設定すると以降絶対パスとして内容を取得できます. 従って, 下記のように一見無意味に見えるプロパティ値の再設定を施すことで相対パスから絶対パスへの読み替えが可能です.
Shadow DOMとは
HTMLではその仕様上, データの構造がそのままスクリーン表示時の構造となります. これは非常にシンプルで理解しやすい考え方でしたが, 表現力に乏しいという面も持ち合わせています. この問題はCSSを用いることである程度までは解決するのですが, あらゆるパターンの要件を充たすには機能不足だったため, 見た目を制御するためだけの要素を用意し, 複数の要素を組み合わせて利用することがよく行われています.
しかしこの方法では, 単一のDOMツリーに多くの要素がぶら下がることとなるため, HTMLコードやDOM構成の可読性を損ないます.
Shadow DOMはスクリーンに表示される要素(ツリー)ごとに「意味的な構造(model)」を「見た目の構造(view)=シャドウツリー」に再構成する ことで, 描画内容を自由に指定可能とします. これはCSSによるスタイルの指定よりもはるかに柔軟に行えるため, 元となるデータ構造をシンプルに保つことが可能です. またシャドウツリーに対しては個別にスタイル付けが可能で, 通常煩雑となりがちなid/class属性の付与やCSS記述を簡略化できます.
ツリー構成上直接見えない構造を"Shadow DOM"と呼称することから, 実際のDOMを"Light DOM"(ライトツリー)と呼ぶことがあります.
その一方でHTML仕様を横断的に拡張していることにより, DOMが本来持っているシンプルさを損なう結果を招いており, 全体像を把握するのが非常に困難になっています. そのため魅力的な機能ながら, 実際の設計・実装フローに組み込むことが難しいといった特徴を持つ, 言わば諸刃の剣です.
内包する構造を隠し, 独自のグラフィック描画を行うという点で, Shadow DOMはcanvas要素の動作によく似ています. しかしcanvas要素にはグラフィックにおける構造が定義されていないため, インターフェースとしての機能を全て自作しなければなりませんでした. Shadow DOMは既存のDOM機構を利用しているだけですから, 既存の仕組みを最大限に活用することが可能です.
補足)仮想DOMとの関係
用語的に似ている概念として仮想DOM(virtual DOM) がありますが, Shadow DOMとは全く異なるものです. DOM構造に見立てたオブジェクト(=仮想DOM)に対する操作を監視し, 変更箇所のみをDOMに反映させることでDOM操作に懸かるコストを軽減します.
そのため仮想DOMはシングルページWEBアプリケーション(ページ遷移を行わず, Ajaxで取得したXMLデータ等を使ってコンテンツを逐次書き換えていく手法)構成時によく用いられます.
従って, 本質的でない部分をシャドウツリーとして隠蔽するShadow DOMとは概念的に連携可能です. 図にすると次のようになります.
なお, 現在仮想DOM機構を提供する標準的な仕組みは存在せず, 専用のライブラリを導入する必要があります.
但し, シャドウツリー構造はDOM操作によって簡単に初期化されてしまうため, 後述するCustom Elements仕様と組み合わせたり, 仮想DOMライブラリ側がShadow DOMを意識した構成となっている必要があります.
Shadow DOM利用の二つのパターン
Shadow DOMの利用に制限はありませんが, 概ね次の二つのパターン, 及びその組み合わせにおいて効果的です.
Shadow DOMでレイアウト構造を与える
複雑化したWEBページ構造をShadow DOMでテンプレート化し, 本質的なコンテンツのみをページに残す方法です. これはHTMLでのフレーム機構, CSSにおけるメディアクエリをより強力にしたものと考えてよく, デザインとコンテンツとを分離することで管理を容易にする他, テンプレートを書き換えることで環境毎に最適な出力を得ることも可能となります.
Shadow DOMによる機能の挿入
既存の要素に新たな構造・機能をShadow DOMで挿入するものです. これまで::before, ::after擬似要素を用いて無理矢理スタイル付けしていた部分に, ボタン等のより具体的な機能を追加することが可能になります. また, 複数のコントロールを一つにまとめて単一のコントロール(カスタムコントロール)とする場合もこちらのケースに分類できるでしょう.
いずれのケースにおいても, Shadow DOMによって挿入される構造が元のツリーと分離されることで, 容易に再利用可能となっている点に注目しましょう.
Shadow DOMの乱用に依る弊害
Shadow DOMによる構造の隠蔽は非常に魅力的ですが, かと言って無意味に使って良いものではありません.
動作パフォーマンスへの影響
HTMLコード上から見えないと言っても, スクリーンへのレンダリング処理時にシャドウツリーの構造を展開することになるため, 未使用時よりも確実に動作パフォーマンスが低下します.
Shadow DOMそのものが持つ複雑性の問題
Shadow DOM仕様はツリー構造の再編成に関わる仕様であり, 理屈上は幾らでも複雑な構造を採ることが可能です. そのため, 無計画にシャドウツリーを生成すると全体としてのツリー構造がどのような経緯で生成されているのかが見えにくくなります.
以上から, スタイル設定だけでは対処出来ないような場合に限りShadow DOMを使うと言ったように限定的に利用すべきでしょう.
Shadow DOMの動作例
最も簡単な例を示します. Shadow DOM機構を用いてdiv要素の中身と異なる文字列をスクリーンに表示しています.
ここで注意すべきはJavaScriptが動作しない場合, 元の内容が表示される点です. 従ってアクセシビリティを考えると, 元の内容を考慮した内容を表示すべき であり, この例はあまり良い使い方ではありません.
次の例は. a要素をそのままにリンク先の画像を挿入したものです. a要素のcreateShadowRootメソッドで生成したShadowRootオブジェクトに, シャドウツリーを定義しています.
このように, 元となるDOM構造にいっさい手を入れずに, 見た目の構造を自由に注入することが可能となります.
template要素との連携
一般にシャドウツリーの構造は複雑になりますが, template要素を使うことでツリー構造をそのままShadowRootオブジェクトに挿入することが可能です.
こもんちゃん
comon@xxx よろしくね!
ここでHTML Importsを思い出してください. このtemplate要素をHTML Importsで外部HTMLファイルから読み込むようにすれば, 元の文書からビュー構造が完全に隠蔽されます. また後述するCustom Elementsでは, 更に上記ロジックをカスタム要素の中に押しこむことでカプセル化することができます. このようにWeb Componentsの機能はそれぞれ単独でも便利なものですが, 組み合わせた際に最大の効果を発揮する のです.
補足)template要素を使わないという選択
template要素を使った場合, 意図した通りにDOMツリーが構成されないケースがある事は示しました. この場合, innerHTMLプロパティを直接操作することで正しく動作するようになります. 下はcontent要素とtr要素をシャドウツリーの中身として利用していますが, 現状template要素を用いるとこの構造が破壊されてしまい, 正しく動作しません.
\ 列1 列2
行1 10 40
行2 20 50
行3 30 60
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
イベントが伝達された経路を取得します.
このようにShadow DOMはWeb Componentsの核をなす技術であり, その影響はHTMLDOMの隅々にまで及ぶという壮大なものです. 従って本格的な利用には今暫く時間が掛かることでしょう.
シャドウツリーの機能
一般にHTML文書を構成する要素はhtml要素をルートとするツリー構造を採ります. ここでツリーを構成するノードの何れかでcreateShadowRootメソッドを実行すると, そのノードに対してShadowRootオブジェクトが生成され, スクリーンに描画するための新しいツリー(シャドウツリー )が作られます. シャドウツリーが生成されると当該要素を描画する際に, 子孫要素の代わりにシャドウツリーの内容が使われる ようになります.
ここで注意すべきは, いかなシャドウツリーを使ったといえどCSSによるDOMの描画はこれまでと全く同じルールで行われる ということです. 単にDOMツリーを手繰る際に, 実ツリーではなくシャドウツリーが使われるようになるだけで, CSSへの影響は(セレクタの記述が特殊になる以外は)特にありません.
シャドウツリーを生成すると, 構造上元のツリーの各ノードから別のツリーが生えているように見えることから, この構造を「ツリーのツリー(tree of trees) 」と呼ぶことがあります. なお, ツリーのツリーは全体として単一のツリー構造をとります.
シャドウツリーへのアクセッサ
createShadowRootメソッドで作成されたShadowRootオブジェクトはElement.shadowRootプロパティから参照出来ます.
シャドウツリーを生成した要素の振舞い
createShadowRootメソッドを実行した要素は, 見た目上子要素がなくなったように振る舞います. しかし, 要素そのものは描画対象となります. つまりdiv要素であればborder等の要素を囲むスタイルで描画されます.
ShadowTree未生成
ShadowTree生成
シャドウツリーへの要素の挿入
createShadowRootメソッドで生成したShadowRootオブジェクトはシャドウツリーのルートノードに相当するものです. ShadowRootオブジェクトに要素を追加すると, シャドウツリーに要素を追加したことになります. ShadowRootオブジェクトはDocumentFragmentインターフェースを実装しているので, 通常のElementと全く同じ感覚で中身を操作することが可能です.
ShadowTree未生成
ShadowTree生成
シャドウツリーと描画範囲
iframe要素やobject要素を用いた場合, 描画されるコンテンツはそれを読み込んでいるノードの描画範囲に収まりますが, シャドウツリーについては必ずしもそうとは限りません. シャドウツリー内のノードについても, CSSレイアウト上はメインツリーに存在するものとして扱われるため, コンテンツに記述されているスタイルによっては, シャドウツリーを保持しているノードの範囲外に描画されることもあります.
シャドウツリーに挿入されたscript要素
シャドウツリー内にscript要素を挿入した場合, 通常の場合と同様にスクリプトが実行されます. またこの時, スクリプトは元ツリーが属するdocumentを開いているwindowオブジェクトをグローバルスコープとします.
これは自然な動作です. なぜならシャドウツリーの所有者はシャドウツリーを保持しているノードの所有者(つまりwindow)であり, script要素はその所有者に対してスクリプトを実行するからです.
下はtemplate要素配下にscript要素を配置し, テンプレート挿入時の初期化を行っています.
シャドウツリーと相性の悪い要素
シャドウツリーにはその役割上, あくまでスクリーン描画のための要素を挿入します. そのため, 描画に直接関わらないlink要素やbase要素と言った, 主にhead要素にのみ含まれる要素はシャドウツリーに挿入しても動作しません. また, select要素配下のoption要素など, レイアウト作用以外に要素の親子関係がその動作に強く影響するもの をシャドウツリーに挿入した場合, シャドウツリーを介することでその結びつきが分断されるため, 動きそうに見えてその実正しく動作しない と言った奇妙な現象を招きます.
つまり, 厳密に言えばシャドウツリーはDOM構造を隠蔽するものではなくレイアウト構造を隠蔽するものなのです. なおShadow DOM仕様では現状一部を除くHTMLの全ての要素がシャドウツリー内においても通常時と同様に振る舞うとされていますが, これは事実上のブラウザ動作と大きく隔たった内容であり, 今後の議論の対象となることでしょう. もし仕様の通りとなるのであれば, Shadow DOMのあり方がまた違ったものとなり得ます. 例えばスクリプトで動的に生成したノードは全てシャドウツリーに挿入して良いことになります.
このような関係にある要素には次のものがあります. 全てを確認したわけではありませんが, これらの要素にシャドウツリーを適用するのは避けたほうが無難でしょう.
select-option
form-input
object-param
picture-img
video-source
map-area
SVGに関わる要素(gやsvg等のコンテナ要素を除く)
その他にも仕組み上子要素を持ち得ない(持ったとしてもフォールバックコンテンツとして表示されない)要素, 例えばmeter要素, progress要素に対しても仕様上はシャドウツリーを持ちうるとされていますが, 条件によっては例外を発生する事があります.
経験的にはdivやsection等の特徴の"薄い"要素ならば問題なく動作します.
シャドウツリーの削除
一度生成したシャドウツリーを削除するAPIは存在しません. そのため, 当該要素を再生成して置き換えるようにします.
当該要素にイベントが登録されていたり生成に複雑な処理を要する場合は, シャドウツリーの中身をcontent要素で書き換えることで擬似的にシャドウツリーの機能を無効化することが出来ます.
なお, 既にあるシャドウツリーを初期化する目的でcreateShadowRootメソッドを利用してはいけません. なぜなら, シャドウツリーは履歴管理されており, 最初のシャドウツリーは依然メモリ上に残っているからです.
シャドウツリーの隠蔽
一度生成したシャドウツリーを隠蔽し, 外部からの変更を受け付けないようにする(完全なカプセル化)ことはJavaScriptの仕組み上できませんが, オブジェクトのプロパティを上書きすることでその頻度を軽減することは可能です.
通常の利用ではシャドウツリーを複数生成することは稀ですから, 一旦シャドウツリーを構築したら上記のようにcreateShadowRootメソッドを上書きして中身を保護するのは悪くない選択です. また関数呼び出し時にエラーを発生させるようにすれば, デバッグにも役立ちます.
シャドウツリーとDOM操作
これ以外にも考慮すべき特徴は存在します.
要素のクローン
cloneNodeメソッド等を使って要素の複製をしたとしてもシャドウツリーまでは複製されません.
シャドウツリーとシリアライズ
シャドウツリーの内容はinnerHTMLプロパティやXMLSerializerなどによる文字列へのシリアライズ処理の対象外です.
シャドウツリーとcontenteditable属性
contenteditable属性はシャドウツリーの内容に影響を及ぼしません.
シャドウツリーとMutationObserver
シャドウツリーへの変更を検知するにはMutationObserverオブジェクトを使って, ShadowRootオブジェクトを監視するようにします. なお次のように記述すればメインツリー・シャドウツリーの両方に対する変更を一括して監視出来ます.
シャドウツリーとスタイルシート
シャドウツリーの導入に伴い, スタイルシートの扱いも拡張されました.
CSSスコープの分離
シャドウツリーにはメインとなるCSSスコープにぶら下がる子CSSスコープが定義されます. 子スコープから親スコープのスタイルを変更することは出来ません. 従ってページ全体への影響を考えること無く自由にスタイルを定義することが出来ます.
逆に親スコープからはフォント設定などのツリー構造を辿って継承される物以外はスタイルが未設定となります. これは一般的なセレクタは子スコープに影響を及ぼさない からです. 従って意識的にスタイル付けをしない限り, シャドウツリー内の要素にはスタイルが設定されないことになります.
このようにメインツリーとシャドウツリーとの間では明確なスタイル設定の境界が存在します. しかし, 使い勝手を鑑みると必要に応じてこの境界を超えたスタイル設定が欲しくなるところです. そこでShadow DOMではセレクタを拡張してこの境界を超えるための仕組みを定義しています. 詳しくは後述します.
シャドウツリーへのスタイル定義
シャドウツリー内部へのスタイル定義には原則style要素を用います. なお, シャドウツリーにはhead要素に対応する仕組みが存在しないため, link要素を使ったスタイルシートの外部参照は出来ません. もしどうしても必要であれば@import命令を利用します.
シャドウツリーへのコンテンツの挿入
ここまでは, 既存のツリーにシャドウツリーを挿入する事を見てきましたが, これとは逆に既存の子要素をシャドウツリーに挿入(投影)する仕組みが提供されています.
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属性によってどのようなコンテンツの取捨選択が為されるかは外部からは判りません. 従って, 明確にレイアウト設計書として文書化しておくべき内容です.
select属性を変更する
content要素によるノードの挿入はスタイルシード等の影響を受けません. 従って, 下のように何らかの状態によって挿入するノードを変更する場合は, スクリプトを記述する必要があります.
シャドウツリー構造を変更する
シャドウツリーの構造は後から変更することが出来ます. 従って予めテンプレートを複数用意しておき, 都度シャドウツリーの中身を書き換えることで自由にレイアウトを変更することができます.
特殊な挿入が行われる要素
Chromeではエラーになります. またFireFoxでの動作を参考としていますが, 仕様的に未だ不明瞭です.
シャドウツリーを持つ要素の種類によっては特殊なコンテンツの挿入が行われることがあります.
挿入ポイントの一般化
シャドウツリーにおける挿入ポイントの考え方を一般化すると既存のHTML要素においても, その要素の直下がコンテンツの挿入ポイント と考えることが出来ます. そうすると, 要素によっては挿入ポイントを複数持つものが存在します. その典型がfieldset要素です. fieldset要素には配下のlegend要素が自動的に見出しの位置に表示されますが, これは一般の要素向けの挿入ポイントに加えてlegend要素専用の挿入ポイントが存在している事になります.
fieldset要素とコンテンツの挿入
ではfieldset要素に対してシャドウツリーを定義した場合, どのようにコンテンツの挿入が行われるのでしょうか? 答えは簡単で, シャドウツリーを持たない場合と同様にノードの挿入が行われます. つまり, (template要素のselect属性で明示されない限り)legend要素は他の要素と区別され, 自動的に規定の挿入ポイントに充てがわれます.
同様の関係はdetails/summary要素にも適用されます.
要素の挿入とスタイル
content要素でシャドウツリー内に(投影)された要素は, メインツリーでのスタイルで描画されます. 詳しくはセレクタの項で解説します.
シャドウツリーの生成履歴とshadow要素
シャドウツリーは複数生成することが可能で, 内部で生成履歴が管理されています. これはシャドウツリーに更にシャドウツリーを挿入するためのもので, シャドウツリー中にshadow要素を指定すると前回生成したシャドウツリーをその位置に挿入することが出来ます. このshadow要素の位置をシャドウ挿入ポイント と呼びます. shadow要素を使うとデザインパターンで言うところのデコレーション機構を実現できます.
従ってシャドウツリーを再構築したい場合はcreateShadowRootメソッドを使うのではなく, 既存のShadowRootの中身を書き換える ようにします.
複数の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つの関係があり, それぞれにアクセスするための仕組みが存在している事が判ります.
この図からはもうひとつ, getDestinationInsertionPointsの内容をroot方向に手繰っていくと, 必ず元ノードの親ノード(parentNode)にたどり着く事が判ります.
シャドウツリーとセレクタ走査
Shadow DOMはその構造上メインとなるDOMツリーに更なるシャドウツリーを追加します. そのため, 既存のセレクタだけでは円滑なノード検索が行えません. そこでShadow DOM専用のセレクタが新たに追加されています.
FireFoxでは現状シャドウツリー内部へのスタイル指定が効きません.
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つの場所で定義できることになります.
style属性によるスタイルの指定
メインツリー内のstyle要素によるスタイルの指定
シャドウツリー内のstyle要素によるスタイルの指定
(:host擬似クラスによるホストノードへの, ::content擬似要素による挿入されたノードへのスタイル指定)
この内, シャドウツリー内で与えたスタイルはメインツリーから与えられたスタイルで上書きすることが可能 です. これは後述するカスタム要素に対するデフォルトスタイルを定義するための仕組み と考えると理解しやすくなります.
main
main+shadow
main+shadow(important)
shadow
シャドウツリーのルートを取得する
:root擬似クラスセレクタは常にメインツリーを参照してしまうため, シャドウツリーのルートノードを取得する用途では使えません. この場合, :host擬似クラスセレクタに::shadow擬似要素セレクタを組み合わせることで, シャドウツリーのルートノード(shadowRootに対応するスクリーン上では意味を為さないノード)を取得できます.
一般に擬似要素セレクタは, 親となるノードに属する仮想的なノードを参照しますから単体では動作しません.
要素がシャドウツリーに含まれているか確認する
このシャドウルートを取得するセレクタ記述を用いると, 当該要素がシャドウツリー内に含まれているかを確認することが出来ます.
但し, 上記の方法は動作が曖昧なため, ツリーを遡ってShadowRootに突き当たるかを判定したほうが確実でしょう.
シャドウピアシング子孫コンビネータ(deepコンビネータ)
セレクタに>>>
を記述することで, 後続するセレクタがメインツリー・シャドウツリーの区別なくマッチするようになります. これをシャドウピアシング子孫コンビネータ と呼びます. この記述は非常に強力な反面, スタイルのカプセル化を破壊する事もあります.
この機能は長らく/deep/
(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
なお, HTMLEventをトリガする(dispatchEventメソッドを実行する)ことで, 下記に示すイベントのリターゲティングを手動で引き起こすことが出来ます.
イベントのリターゲティング
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プロパティと全く同じです.
ChromeではStyleSheetオブジェクトを上手く取れないようです.
シャドウツリー内のアクティブな要素を取得する
Document.activeElementプロパティには現在フォーカスを持っている(アクティブな)ノードが格納されていますが, このノードが更にシャドウツリーを持っていた場合は, ここから更にShadowRoot.activeElementプロパティをたどることでシャドウツリー内部でのアクティブなノードを取得できます.
なお, 現在ChromeとFireFoxとで動作が異なっています. FireFoxではシャドウツリー内部のアクティブなノードを直接取得できてしまいます.
指定した座標でシャドウツリー内を検索する
Document.elementFromPointメソッドを用いると, 指定した(ビュー範囲の左上から計った)位置に見えているノードを抽出できます. ここでシャドウツリー内部に存在するノードまで詳しく調査したい場合はShadowRootオブジェクトの同名のメソッドを利用します.
なお, 現在ChromeとFireFoxとで動作が異なっています. FireFoxではシャドウツリー内部のノードを直接取得できてしまいます.
シャドウツリー内で選択された範囲を取得する
Window.getSelectionメソッドを用いると現在選択中の内容を取得できますが, シャドウツリー内部の選択範囲を取得するにはShadowRoot.getSelectionメソッドを使います. しかし現状では仕様も曖昧であり, ブラウザ側の動作も今ひとつ納得出来るものではありません.
FireFoxではシャドウツリーの内容は選択出来ないようになっており, これは::before/::after擬似要素の動作に準じています. Chromeではツリーを跨いだ選択の可不可が不明瞭で, 却って判りにくいものになっています.
シャドウツリーにおけるリンクとid属性
シャドウツリー内にa要素を配置しリンク機能を挿入した場合, そのリンクは一般のa要素と同様に有効となります.
a要素の参照可能範囲
a要素が参照できるリンク先はメインツリー内に配置されている必要があります. つまりシャドウツリー内にリンクのhref属性値と合致するid値の要素が含まれていたとしても, それは見た目だけであり, 実体が無いためリンク対象とはなりません. 逆にcontent要素やshadow要素によってシャドウツリーに投影されている要素は, 実体が存在するためリンクの対象となります.
in→in (same) link target
\ anchor href
in→in (other)
in→out target
in→out (wrap) target
out→in link
なお, スクリプトを用いてシャドウツリー内を検索し, 強制的にフォーカスを与えたとしても, 「:target」擬似クラスは有効となりません. (「:focus」擬似クラスは有効です).
SVGのxlink:href属性とシャドウツリー
(インライン)SVGではHTMLでのhref属性と同様にxlink:href属性が様々な役割を担っています. 例えばuse要素ではこの属性が参照している図形ノードを複写することが出来ます. しかし先ほどのリンクの例とは異なり, use要素によるノードの参照はツリー内を直接検索するため, 参照先が同一ツリー内に存在しないと正しく動作しません.
same tree
shadow→main
main→shadow
これらのことからシャドウツリーにおけるid属性の扱いは少なくとも単一のシャドウツリー内で一意の値が取れれば十分 ということになります.
シャドウツリーとタブ順
シャドウツリーを用いたWEBページにおけるタブ順は, 原則的にメインツリーとシャドウツリーの順序がミックスされ, 見た目に沿った自然な順序となります.
しかし, 現状タブ順については上手く機能していないようです. 特にシャドウツリー内に設定されているtabindexは全く動作しません.
フォーム機構とシャドウツリー
ここまででスクリーンに描画される内容がシャドウツリーに置き換わることが判りました. では, フォーム部品をシャドウツリーで上書きしたり, 挿入したらどのような結果となるでしょうか?
結論から言うと, フォーム部品の機能にシャドウツリーの内容は影響しません. つまり, フォーム部品の見た目をシャドウツリーでオーバーライドしても, 基本となるフォーム部品の機能は失われず, データ送信の対象となります. また, シャドウツリー内部にフォーム部品を挿入しても, シャドウツリーの外側のform要素からその内容を確認出来ませんからデータ送信の対象とはなりません.
シャドウツリーを使ったフォーム機能の注入
form要素のフォーム送信が有効となるには「form要素」と「submitボタン」が同じツリー上に存在している必要があります. 次の例ではform要素がメインツリーに, サブミットボタンがシャドウツリー上にあるため, 見た目上は動作しそうですが上手く行きません.
form要素をシャドウツリー内に移動すると, フォーム送信が有効となります.
フォーム部品とシャドウツリー
input要素の表示内容をシャドウツリーで上書きすると新たなフォーム部品を生成することが可能となります. 次の例ではシャドウツリー内にラジオボタングループを生成しています.
ここでShadow DOMをサポートしない環境やJavaScriptが無効な環境では単なるテキスト入力に見える 点に着目しましょう. つまり, 基本的な機能をそのままに, 環境に応じて最適な入力フォームを構築できるのです.
フォーム部品作成時の注意点
とは言え, 実際にフォーム部品を作るとなるとそれなりに考慮すべき点が出てきます. 以下は筆者が気付いた点です.
input要素にシャドウツリーを追加すると, input要素標準の動作が全てキャンセルされます.
つまり, クリックやキー入力による状態変化処理が一切無効となります. 従ってこれらの処理を全て自作する必要があります.
シャドウツリー内からは元となるinput要素の状態を:host()擬似クラスセレクタで取得可能です.
従って, これを::shadow擬似要素セレクタと組み合わせることでシャドウツリー内部のスタイルを変更することが可能です.
コントロールの見た目をCSSのappearanceプロパティを用いて変更可能とします.
但し, このプロパティは標準的なものではありません.
シャドウツリー内に新たなinput要素を設ける場合は, 最終的に親となるinput要素の状態を書き換えます.
以上を踏まえた上で, チェックボックスをオーバーライドしてみましょう.
他の部品についても, 多少の処理の追加が必要となるでしょうが, 概ね同じような形で拡張することができるでしょう.
しかし, 現状では正しく動作するためにはベンダプレフィクス付きのスタイルを指定せねばならず, 果たしてこのまま使い続けて良いものやら疑問も残ります.
同様のことはCSSでも実現可能であり, 単発での利用毎にこのようなコードを記述するのは余りに無駄です. 後述するカスタム要素としてinput要素を継承して使いまわすべきです.
もし, カスタマイズしたフォーム部品を他のライブラリ等と組み合わせる場合は, 常にフォーム部品としての機能を維持していることを確認してください. 例えば, 入力ができる, フォーカスがあたる, readonly/disabled等のプロパティが正しく動作する, 各種イベントを正しく発行する, :checkedなどの擬似クラスセレクタが有効となるなどです.
フォーム部品上書きのアンチパターン
フォーム部品の内部を不用意に書き換えてしまうと非常に厄介な問題を引き起こすことがあります. 次の例ではわざと見た目の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やスクリプトから自由にアクセスできるからです. 但しシャドウツリーの内部からは外部のノードを参照することが出来ない点に注意しましょう.
なおSVGファイルの読み込みにHTML Importsが使えないため, Ajax機構を使って動的に読み込むか, object要素で一旦SVGを読み込み, 更にシャドウツリーにその内容をコピーすると言った工夫が必要になります.
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を使って, これがどのような役割を担っているか, どのような状態にあるかを宣言しましょう.
role属性等の記述はカスタム要素を使う側でも考慮する必要があります. そのためカスタム要素の提供者は想定している記述のサンプルを示すと良いでしょう.
とは言え, 通常プロダクトには個々に実現すべき要件が存在しており, Web ComponentsにしろWAI-ARIAにしろその要件を実現するための手段に過ぎません. 従って動作環境が限定されているのであれば, 無理にアクセシビリティを考慮する必要は無いでしょう.
補足)カスタム要素定義の方法に係る議論
現在のCustom Elements仕様では伝統的な「prototypeチェーン」によるオブジェクト継承によってカスタム要素を定義することを想定しています. しかし並行して仕様検討が成されているECMAScript6(JavaScript実装に対する次期標準仕様)では, 新たに「class/extends宣言を使った継承 」機構が追加される予定です. このことからCustom Elements仕様はECMAScript6仕様と整合性がとれていないとの意見 があります.
Custom Elements仕様においても決してECMAScript6を蔑ろにしている訳ではなく, ECMAScript仕様の確定後に改めて仕様の詳細化をするとのことですが, その結果, 先行実装している側(Chrome/FireFox)とそうでない側(Safari)との間に温度差が生じています.
カスタム要素の定義の流れ
カスタム要素の定義・生成は概ね次の流れに沿って行われます. 一見複雑に見えますが, 本質的に通常JavaScriptで行われているオブジェクトの継承手順と全く同じです.
カスタム要素の名称と継承元(HTMLElement/SVGElement)を決定します.
Object.createメソッドを用いてカスタム要素のプロトタイプを生成します.
プロトタイプにライフサイクルコールバック関数やプロパティを登録することで, カスタム要素としての振舞いを定義します.
document.registerElementメソッドを使ってカスタム要素をDOMに登録し, コンストラクタを取得します.
コンストラクタを使ってカスタム要素オブジェクトを生成します.
カスタム要素オブジェクトをDOMに挿入します.
先程の例と照らしあわせてみましょう.
基本方針としては, select要素配下のoption要素等の文書構造上意味のある要素については直接の子として配置します. それ以外の(style要素を含む)レイアウト目的の要素は原則シャドウツリーに押しこむようにします.
カスタム要素の命名規則
カスタム要素に付ける名称は次の条件を満たす必要があります.
カスタム要素として一意であること
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
」と言ったように中身の内容が判るものにしましょう. また, 全体としての命名ルールを設けるのも効果的です.
逆にこの「my-element-001」が他のシステムとの連携をする際に有利なものであれば無理に変更する必要はありません.
また, 将来的にカスタム要素をパッケージ化し公開することを考えているのであれば, 他のカスタム要素と名称が衝突しないように工夫する必要があります. しかしその標準的な手法についてはまだ議論されていません.
カスタム要素の命名ルール不在に依る問題は既に起こりつつあります. 通常このような問題に対しては名前空間と呼ばれる仕組みで対処しますが, HTML(XML)におけるnamespace機構はCustom Elementsが(X)HTML名前空間を拡張する技術であることから, そのままでは使うことが出来ません. また, (Javaのパッケージ名のように)要素名に出自を入れておくのも, 要素名が冗長となることでかえって使い勝手を損ないます.
そこで, 筆者はカスタム要素名を使う側で設定できるようにしておく事を提案します. カスタム要素はregisterElementメソッドを実行するタイミングでその役割が確定します. 従って, カスタム要素名を何らかのパラメータとして外部から渡せるようにしておくのです. ですが依然パラメータの渡し方をどう(統一)すべきかについての議論が残ります.
カスタム要素のプロトタイプを生成する
WEBブラウザに表示されているオブジェクトは全てElementオブジェクトを継承しており, 全体としてDOMツリーと呼ばれる木構造を形成しています. カスタム要素はこのDOMツリーに挿入されますから, 少なくともこのElementオブジェクトを継承する必要があります. 次の図はオブジェクトの継承関係を示しています.
カスタム要素はこの右側のツリーのどのオブジェクトをも継承することが出来ます.
HTMLDOMでは二つの意味の「ツリー構造」が存在しています. この図では左側のツリーはドキュメントを構成するツリーであり, 主にShadow DOMではこの文書構造のツリーを拡張する仕様でした. 一方の右側のツリーはノードの機能(継承関係)に関わるツリーであり, 上位に行くほど抽象的な概念を表しており, 各要素共通の機能を備えています. Custom Elementsはこの継承関係のツリーをユーザー側で拡張するための仕様と言えます. ここでHTML Importsはここにリソース参照のツリーと言う新たな概念を導入していると考えると, Web Components仕様はいずれもツリー構造の扱いに関わる仕様ということになります.
継承元のオブジェクトを生成する
JavaScriptにおける継承は, プロトタイプチェーン と呼ばれる仕組みを使って実現します. つまり継承元オブジェクトのインスタンスを新たなオブジェクト定義のプロトタイプとして扱うことで(擬似的に)継承を表現します.
JavaScript(現行のECMAScript5)ではJavaやC♯のようなタイプベースのオブジェクト指向言語と異なり, 言語レベルでのオブジェクト継承機構が定義されていません.
さて, カスタム要素を既存の要素を拡張するのではなく一から定義する場合, 通常HTMLElementオブジェクトを継承するようにします. しかし, HTMLElementには直接的なインスタンスの生成機構(コンストラクタ)を持たないので, 汎用のオブジェクト生成API(Object.create)を使う 必要があります.
Object.create(prototype, properties)
指定したプロトタイプから新たなオブジェクトを生成します. propertiesには追加したいプロパティの設定情報を指定します.
myElementProtoはHTMLElementオブジェクトですから, Element/HTMLElementが定義しているプロパティ・メソッドは全て利用可能です.
補足)継承元オブジェクトの選択
この時作りたい要素が文脈上既存のHTML要素を継承しているとしたほうが自然な場合は, 極力その要素を継承してください. これは実際のHTMLコードをイメージすれば実に当たり前のことです. 例えば, ol要素配下においてli要素のように振る舞うカスタム要素を定義するとします. この場合,
と記述するよりも,
としたほうがよりカスタム要素の役割が判りやすくなります. また, SVGに属するカスタム要素を定義する場合は, 既存のSVG要素からカスタム要素を派生させる必要があります. 詳しくは後述します.
カスタム要素の振舞いを定義する
上記で得られたプロトタイプオブジェクトには, カスタム要素として振る舞うための処理を記述していきます.
ライフサイクルコールバック関数を定義する
プロトタイプオブジェクトに特定のプロパティに関数を登録しておくと, カスタム要素に対する操作を行った際にDOMツリーから自動的に呼び出されるようになります. これをライフサイクルコールバック関数 と呼び, 次の4種類が存在します.
createdCallback()†
カスタム要素のインスタンスが生成された時, もしくはDOMに存在しているカスタム要素候補にカスタム要素定義が結びついた際に実行されます.
attachedCallback()
カスタム要素が(現在表示中の)documentオブジェクトに挿入された際に実行されます. DOMの構造を意識する必要がある場合に便利です.
※「現在表示中」とは直接ドキュメントを開いている場合や, iframe要素等からドキュメントを表示している場合を指します.
detachedCallback()
カスタム要素が(現在表示中の)documentオブジェクトから離れた際に実行されます.
廃棄すべきデータの
attributeChangedCallback(attrName, oldValue, newValue, namespace)
カスタム要素に対する属性値に追加・変更・削除があった際に実行されます. 引数の内容でどのような変更があったのかが判定できます.
状態 attrName oldValue newValue namespace
追加 ○ null ○ ○
変更 ○ ○ ○ ○
削除 ○ ○ null ○
なお, 属性値を同じ値で上書きした場合(つまりsetAttribute("xxx", getAttribute("xxx"))
)にattributeChangedCallbackは呼び出されません.
† FireFoxではcreatedCallbackが登録されていない場合, attachedCallbackの呼び出しが無視されるようです.
内部コンテンツの変更についてのコールバックがありませんが, これはMutationObserverを使って対処します.
例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ツリー内のカスタム要素候補との紐付処理が為されるためで, この紐付をやり直すことが出来ないからです. 同様にメソッドの実行を取り消すことは出来ません.
カスタム要素レジストリはwindowにつき一つです. 従ってHTML Importsを使ってサブ文書を読み込んでいた場合, メイン文書・サブ文書間でカスタム要素レジストリは共有されます.
引数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メソッド)を使って生成しますが, コンストラクタを使うとこの長い記述をせずに済みます.
実は既に似たような仕組みは存在しています. ImageオブジェクトはHTMLImageElementのコンストラクタを与えているのです.
カスタム要素のライブラリ化
生成したコンストラクタをそのまま使っても良いですが, 用途や役割毎にまとめておくとライブラリとして活用しやすくなります.
カスタム要素を更に継承する
自作したカスタム要素をベースとし, 新たなカスタム要素を生成することが出来ます. プロトタイプチェーンを辿り, 継承元のメソッドをオーバーライドすることで機能を追加します. この場合, 継承元で作成されたシャドウツリーを装飾する場合はshadow要素を用いるとよいでしょう.
一般的にプロトタイプチェーンによる継承を繰り返すと動作パフォーマンスに影響を及ぼす他, 動作検証が難しくなるため, 慣れないうちは避けたほうが無難です. prototypeオブジェクトのファクトリを定義し, 共通処理を一箇所にまとめたほうが判りやすいでしょう.
HTMLのパーシングとカスタム要素
カスタム要素は既存のHTML要素と同様に予め文書中に記述しておくことが出来ます. しかしカスタム要素の定義はスクリプトによって行われるので, 定義処理の前にDOMツリー内にカスタム要素(の候補)が存在している可能性があります. そのため, カスタム要素が正しく解釈されるために特殊なHTMLパーシング(HTMLのコードからDOMツリーを構成する処理)が行われます.
タグ名によるインスタンス化の振り分け
HTMLのパーシング処理では, 通常解釈出来ないタグ名の要素が見つかった場合, HTMLUnknownElementオブジェクト(つまり不明な要素 )としてDOMツリーに挿入します. しかしCustom Elementsによる機能拡張により, タグ名にハイフンが含まれている「カスタム要素になりうる要素(これを未解決の要素 と呼びます)」についてはHTMLElementオブジェクトとして扱います.
Custom Elementsをサポートしない環境では全てHTMLUnknownElementとして解釈されます. 従って今後HTMLUnknownElementに依存したコードが正しく動作する保証はありません. しかしこのようなケースは極めて稀ですから気にしなくてもよいでしょう. なお, HTMLUnknownElementそのものには独自の機能は備わっていません.
カスタム要素機能の注入
document.registerElementメソッドが実行されると, DOMツリー内から未解決の要素のうちタグ名と引き数のカスタム要素名とが一致するものが抽出され, 初期化処理(ライフサイクルコールバック関数の呼び出し)が行われます. その際createdCallback→attachedCallbackの順で処理が呼び出されます. この処理は同期的に行われます.
これは任意のタイミングでカスタム要素機能をロードできるということです. カスタム要素が多数含まれている場合や, 初期化処理に多量のリソースを要すると言ったケースにおいては, WEBページのロードが完了してからゆっくりと行うと言った工夫が必要となるでしょう.
ツリー構造とカスタム要素の初期化順
registerElementで登録したカスタム要素が文書中に複数存在した場合, DOMツリーにおける自然なノード順(≒HTMLコード順)にコールバック関数が呼び出されます. 次の例では同名のカスタム要素が入れ子となっていますが, この場合は親ノードから子ノードの順に初期化されます.
また, 複数種類のカスタム要素が親子関係を持つ場合, カスタム要素の登録順でコードの成否が変化することがあります. 次の例では子要素から親要素の内容を参照しているため, 初期化前の親要素を参照しているか否かで動作が変化します.
この問題を回避するために, 予めどのような順にカスタム要素を定義すべきかについて検討しておきましょう. またカスタム要素の定義順に依らない設計 としておくことも大切です.
複数種のカスタム要素が相互に関連し合う場合, カスタム要素の役割付けにobserver, mediatorと言ったデザインパターンを導入できるかも知れません.
:unresolved擬似クラスとFOUCの回避
未解決の要素にカスタム要素としての機能が注入されるまでの間, スクリプト処理によって再レイアウト処理や再描画処理が発生します. そのため複雑なノード操作をする際は, この必要のない動き「=ちらつき(FOUC:Flash of unstyled content)」を回避する必要が出てきます.
ちらつきの原因には次の二つが考えられます.
カスタム要素のサイズが変化することに依る再レイアウト
カスタム要素の内容が変更されることに依る再描画
前者については予めカスタム要素に固定的なサイズを指定しておくことで解決します. 後者については次に示す:unresolved擬似クラスセレクタを利用します.
:unresolved擬似クラスセレクタ
:unresolved擬似クラスセレクタはCustom Elementsで定義されたもので, 未解決のカスタム要素(つまり「xxx-xxx」形式の未知の要素)にマッチします.
:unresolved
未解決のカスタム要素候補にマッチします
解決済みのカスタム要素
未解決のカスタム要素候補
不明な要素
この:unresolved擬似クラスに対して「visibility:hidden
」や「opacity:0
」を指定しておくことで, カスタム要素登録中のちらつきを隠すことができます. なお「display:none
」を指定した場合はページの再レイアウト処理を誘発するため避けたほうが無難です.
:unresolved擬似クラスはカスタム要素を使う側で指定します. なぜなら, カスタム要素を定義する側では要素定義の前の環境を操作することが出来ないからです.
カスタム要素とエラーノードの再配置
但し, カスタム要素もHTMLのパーシングルール上は不明なノードに該当します. 従ってtable要素のように内部に記述可能なノードの種類に制限があった場合, DOMツリーへのパーシング時にエラーノードの再配置によって意図した位置にノードが挿入されない と言う現象が発生します.
これらの問題は, 既存の要素を継承することで対処可能です.
この問題はHTML特有のものであり, XHTMLを用いた場合は発生しません.
SVGとカスタム要素
SVGではDOM中に現れた不明なノードをスクリーン描画処理上常に無視する ことになっています. この動作との整合性を鑑み, SVGにおいてカスタム要素を定義するには既存のSVG要素を継承することとされています. Custom Elementsのサポート有無で大きく見た目が変化してしまうのは望ましく無いからです.
既存の要素を継承する
カスタム要素を定義する方法にはもうひとつ, 既存の要素を継承する 方法があります. 既存の要素を継承することで, HTML上の文脈やパーシングルールを維持しながらカスタム要素としての機能を追加することができます. 継承の仕方はこれまでとほとんど変わりませんが, 2箇所変更すべき点があります.
プロトタイプオブジェクトを生成する際に, 継承したい要素オブジェクトのprototypeを用います.
registerElementメソッドの第二引き数のextendsプロパティに継承した要素のタグ名を追加します.
次はbutton要素を拡張する例です.
ボタン
同様にSVGに属する要素を拡張することができます.
この仕組みはHTMLの文脈やパーシング処理の整合性を保つためのものですから, 独自に定義したカスタム要素を更に拡張する用途においては意味をなしません.
専用のインターフェースを持たない要素を拡張する
HTMLの要素の中にはHTMLDOM内に専用のインターフェースが用意されていないものがあります. 例えばsection, article, nav等の要素はDOMツリー内ではHTMLElementオブジェクトとして展開されます. 従ってプロトタイプオブジェクトの生成にはHTMLElement.prototypeを用います.
API上の結果は通常のカスタム要素生成と変わりありませんが, 文脈上の意味合いを引き継ぐ事が出来ます. なお, registerElementメソッドにおいてextendsプロパティを指定しなければならないのはこのためです.
既存の要素を継承するための拡張仕様
既存要素を継承した場合, 元のタグ名と異なるタグ名を付けてしまうと先ほどの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要素を継承しているカスタム要素の名称を指定します.
従って既存のJavaScriptライブラリとカスタム要素とを連携させる場合は, 上記メソッドを利用している部分の仕様の拡張が必要になります.
is属性の扱い
is属性はElementオブジェクトの生成時に一度だけ読み込まれます. この時のis属性の内容によって, 既存のHTML要素か, カスタム要素候補か, カスタム要素かの判定が為されます. 従って後からis属性に内容を設定・変更したとしても, それは既存要素を継承したカスタム要素とは解釈されませんし, 別の型のカスタム要素とは解釈されることもありません. 従って, is属性は属性でありながらタグと同様に後から変更することはできません.
サンプルコードから判るように, 当該is属性を持つ要素を検索してその内容を書き換えた上で(innerHTMLを使って)DOMを再構成すれば, 見た目上はカスタム要素の読み替えは可能です. この場合, 暗黙の内に廃棄されるElementオブジェクトがガベージコレクションを引き起こす可能性に注意してください.
継承と型判定
一般的に行われているinstanceof演算子やtypeof演算子による型判定は, カスタム要素でも可能です. なお, 利便性を鑑みるとtoStringメソッドをオーバーライドしておくと後々扱いやすくなるかも知れません.
未解決)未定義の要素を独自実装する
今後HTML仕様に要素が追加された場合, その内容を先取りしてカスタム要素で実現する場合を考えます. すると, 新たな要素をHTMLに記述しておき, その要素をカスタム要素機構で継承する形を採ることで, 理論上将来ブラウザがその要素をサポートした際に何も変更すること無く使い続けることが可能です.
しかし, 現状FireFoxではextendsプロパティに既存のタグ名のみ指定可能であり, 具体的なフローについてはよく判っていません.
カスタム要素へのスタイリング
カスタム要素のスタイルは継承元の要素のスタイルを引き継ぎます. なお, HTMLElementを継承した場合は, スタイルが未指定となります. (シャドウツリーを含む)配下に要素が無ければ全く表示されませんが, 適切にスタイルを指定することで他の要素と同じように扱うことも可能です.
基本的にXML文書に対するCSSスタイリングと同じと考えられます. 従って::before/::after擬似要素の動作は環境に依存します.
属性セレクタを使ったカスタム要素判定
既存の要素を継承している場合は, 属性セレクタ「[is=[カスタム要素名]]
」を使ってカスタム要素を選別することが可能です.
スクリプトから継承した要素を挿入してもセレクタが有効になります. つまりis属性を適切に設定していることになります.
カスタム要素とカスタムイベント
カスタム要素を定義する上で, カスタムイベント機構は非常に相性の良い仕組みです.
カスタムイベントを内包する
カスタムイベントはユーザーが自由に定義できるイベントです. 既存の(click, mouseover等の)イベントでは(処理上は十分なものの)具体的に何を通知したいのかが判りにくい場合があります. カスタムイベントではこの名称や通知するデータについての裁量が使う側に委ねられるため, 柔軟なイベント設計が可能となります. 更にカスタム要素と組み合わせると, このカスタムイベントの定義・着火機構を内包させることが出来る ため, 全体としての使い勝手が向上します.
CustomEvent(type, eventDict)
カスタムイベントオブジェクトを生成するコンストラクタです. typeにはイベントの名称を, eventDictにはイベントの特性やリスナー側に通知したい内容を設定します.
detail
イベントのリスナー側に通知したい内容を設定します.
canBubble
イベントをDOMツリーの上位ノードに通知(バブルアップ)するかどうかの真偽値です.
cancelable
イベントをキャンセルできるかの真偽値です.
Element.dispatchEvent(event)
イベントを着火し, 登録済みのリスナー関数の実行を促します.
下の例では定期的にticイベントを発生するmy-timer要素を定義しています.
カスタムコントロールとカスタムイベント
このようにカスタムイベントによる通知処理はイベントそのものに固有の名称をつけられるため, シャドウツリーを用いて複数のフォーム部品を一つにまとめる(つまりカスタムコントロールを構成する)場合に効果的です.
Web Componentsでプラグインを構成する
最後のまとめとして, Web Componentsの仕組みを総動員して簡単なプラグインを作ります. 全ての仕様が過不足無く連携していることが判るでしょう.
更に, 元のHTMLではプラグインの導入とカスタム要素の記述だけで済んでいる 点に着目してください. そのため, 複数のページで同じライブラリやカスタム要素を使いまわすことが非常に楽に出来るようになります.
具体的な手順
プラグインを構成する場合の手順を見てみましょう.
プラグインを定義するHTMLファイルを用意します. 外部ライブラリを必要とする場合は, ここに記述します.
[Custom Elements]script要素を用意し, カスタム要素を定義するためのスクリプトを記述します.
[Shadow DOM]プロトタイプオブジェクトのcreatedCallbackにシャドウツリーの構築ロジックを挿入します.
[template要素]シャドウツリーに挿入するDOM構造はtemplate要素として定義しておきます. template要素にはスタイル情報を挿入しておきます.
[HTML Imports]プラグイン化したHTMLファイルをlink要素から読み込みます.
プラグイン作成時の検討事項
個人で利用する場合はともかく, 作成したプラグインを一般に公開するのであれば, 既存の環境を汚染しないような対処が必要です. 例えば次のような点に注意します. こうすることでプラグイン間の干渉を未然に防ぎ, より使いやすいプラグインとなります.
極力グローバル変数を使わない
(windowオブジェクトが管理している)グローバル変数は様々なライブラリやスクリプトが共有しています. 従ってプラグイン側では全機能を単一のnamespaceオブジェクトに詰め込み, 1グローバル変数で全てが賄えるようにします. (先程の例では「MyPlugin
」がnamespaceオブジェクトです.) また, この変数名を動的に変更できるようにしておくと, 万が一グローバル変数名が干渉した際にも対処しやすくなります. カスタム要素のタグ名称についても全く同じことが言えます.
特定のライブラリ・バージョンに依存しないように記述する
作成したプラグインが既存のスクリプトライブラリに依存していた場合, 他のプラグインとの干渉する可能性があります. なぜなら組み合わせたプラグインにおいても, 何らかのライブラリを内部で読み込んでいる可能性があるからです. また同名のライブラリを利用していたとしても, 細かなバージョンの違いから正しく動作しないと言った問題が発生し得ます.
従ってプラグインを構成する際は原則としてWeb Componentsの標準機能だけを用いるようにし, どうしてもライブラリの導入が必要な場合は, その有効範囲がプラグイン内部のスコープに限定されるように工夫しましょう.
jQuery等の単一のnamespaceオブジェクトを専有するタイプのライブラリについては, namespaceオブジェクト全体を交換することが出来るため, スクリプトモジュールのサイドバイサイド実行(異なるバージョンの同名ライブラリを並行して実行すること)は比較的容易です. 逆にprototype.js等の環境を書き換えるタイプのライブラリについてはこれが不可能であることから, WEB部品を構成する用途においては不適ということになります. いずれにせよ, Web Components環境から利用されることを想定しているわけではありませんから注意が必要です.
プラグイン全体としてのファイル数を減らす
動作に必須となるデータが個別のファイルとして管理されていた場合, プラグイン毎にその配置を検討せねばならず, 結果として利用者側の管理コストを増大します. よって, プラグインを構成するスタイル・スクリプト等は出来る限り単一のHTMLファイルにまとめるようにしましょう. また, 画像等のデータもURIデータスキーム形式としてファイルに埋め込むことが出来ます.
結局の処, スクリプトライブラリ作成時のノウハウはWeb Componentsを使う際においても有効です.
このようにメイン文書の構造を出来る限りシンプルに保ちつつ外側から多彩な機能を注入できる, それこそがWeb Componentsが目指している未来なのです!
これでWeb Componentsの解説は終了です! お疲れ様でした.