Web Componentsについて勉強した

Web Componentsについて勉強した

元々は別の記事(執筆中)のいちトピックとして取り扱うつもりでしたが思った以上に文量が膨れ上がってしまったため個別記事として独立させることにしました。

「『Web Components』ってのをなんか勉強しといた方がいいらしい」とは聞いたものの、実際どんなもんか触れてみる機会が特になかったので、エアプなりに調べたり試した結果の備忘録的な記事になります。

エアプなので間違ってても許してくれや。。。

Web Componentsとは?

普段ウェブサイト制作やECサイト運用などをメインにやっていると聞き慣れない言葉かもしれませんが、Web Componentsという技術があります。

ウェブアプリ開発なんかではデファクトスタンダード的な存在となりつつある(らしい…)ので、ウェブ屋としての食い扶持をつなぐためにも学んでおくとよいでしょう。

このWeb Componentsというのは、「Webページ上の複雑怪奇なHTML/CSS/JS等の記述に影響を受けない、かつ、どのページでもいい感じに再利用できるような『コンポーネント(≒部品)』を使おうぜ」という仕組みになります。

Web Componentsを実現する上で、「Shadow DOM」「カスタム要素(Custom Elements)」「HTMLテンプレート」という三つの技術を組み合わせて使うことが多いです。

Web Componentsのイメージ

これらの技術は全て一般的なモダンブラウザ(つまり、IEや昔のAndroid Browser以外のこと)で動作するので、2021年現在では気兼ねなく活用可能なものといえるでしょう。

※本記事では、「そもそもDOMとは何か」「ES6(以降)におけるJSの書き方」に関する説明を省いています。あしからず。

Shadow DOMが創る陰の世界

まず最初にShadow DOMについて説明します。

こいつの特徴を一言で言い表すならば、「カプセル化」という性質にあります。このカプセル化とは、要するにページ内に適用されるCSSやJSの影響を受けない(例外あり)独立した存在であるというものです。

Google MapsやYouTubeなどをiframe要素で埋め込んだ際、要素内のコンテンツには干渉できないじゃないですか。イメージとしてはあれに近いです。

HTML上に表示されているのはiframe要素だけであり、外部からは中身を制御することもできない。しかし実際には様々な機能が組み合わさって出来ている

iframe要素は原理的に「別のウェブページを埋め込み表示」させているので諸々融通が効かない点があるのですが、Shadow DOMはDOM特有のある程度の柔軟さも持ち合わせているのが強みと言えるでしょう。

なお、このカプセル化されたShadow DOMと区別する意味で、今まで我々が扱っていた普通のDOMは便宜的にLight DOMと呼ばれています。たとえば、Shadow DOM内でLight DOM上にあるクラス名と同じ名前のクラス名を使用してもそれぞれ別物として扱われる(Light DOM⇔Shadow DOMでスタイル等が影響し合うことはない)といった性質があります。

Light DOMとShadow DOMは互いに影響しない

Shadow DOMという概念についてもっと分かりやすく明快な説明が欲しい方は、CodeGridのShadow DOMが生まれた理由という記事なんかが分かりやすいのでおすすめです。

実践Shadow DOM

理屈の説明は一旦さておいて、試しに使ってみるとしましょう。Element.attachShadow()を使うとShadow DOMの追加ができます。

で、上記ソースの実行結果は下記のようになります。

結果例

上記の例でいうなら、「Light DOM上の.textとShadow DOM上の.textは同名の異なる存在扱いだからそれぞれのスタイルが影響し合うことはない」し、「shadow内に書かれてるDOMはdocument.querySelector()とかでアクセスすることもできないよ~」といった具合です。

「日本語でおk」という方のために、箇条書きでまとめます。

  • Shadow DOMとは、Light DOMから隔離されて自己完結している存在だよ(カプセル化)

  • Shadow DOMはLight DOMに一切干渉しないよ

  • Light DOMがShadow DOMに影響を及ぼすことは「基本的に」ないよ

  • でもLight DOMから「意図的に」Shadow DOMをカスタマイズすることはできるよ例)slot要素(後述)を使ってLight DOMの内容をShadow DOMに挿入する、Shadow DOM内の要素にLight DOMのスタイルを継承させる、など

:host擬似クラス

一般的なCSSファイル上で使用されることはない、特殊な擬似クラス:hostを紹介します。

これはShadow DOMのホスト要素を選択します。Shadow DOM外で使用しても効果がないため、Shadow DOM内にベタ書きされているか、Shadow DOM経由でインポートされているCSSファイルに書かれている場合以外で見かけることはないでしょう。

「ホスト要素って何すか」って? そのShadow DOMのルート…つまり、例えるならShadow DOM要素版のhtml {}みたいな感じです。イメージが湧かない人はサンプルソースとその結果画像見てもらえばなんとなく分かると思います。

お手軽テンプレート作成 template要素

template要素(<template>)を使うことで、「ページの読み込み時にすぐには描画されないものの、JS経由で再利用可能なHTMLを保持する仕組み」を活用できます。簡単に言うと「テンプレートとして使いたいHTML郡を定義するための要素」ってところ。テンプレートエンジンに近いイメージですが、PHPやJSライブラリに頼らずとも実装できるのが利点と言えるでしょう。

なお、このtemplateタグで囲われた範囲もShadow DOMと同じ扱いになります。Light DOM上内明示的なShadow DOMを生成する要素ともいえます。

こんな感じで書くと

ってな具合で出力されます(<template></template>内は画面上に描画されません)。

「後から要素を追加する」? slot要素

追加したShadow DOMやtemplate要素はLight DOMによる干渉を受けないという利点があるのですが、iframeのように全く外部からどうすることもできないとなるとそれはそれで厄介です。

たとえば、HTMLの”構造”自体はtemplate要素でまとめたいけど、その構造内に入る”内容”までtemplate要素内に記述してしまうと諸々の取り回しが悪くなってしまうとか。

このような場合はslot要素を使って解決を図ります。

このような場合、開発者ツール上では

上記のように表示されますが、画面上での実質的な表示内容は

に等しいものとなります。後述のカスタム要素で使用されることも多いでしょう。

::slotted()疑似要素

スロット内に配置された要素を選択してスタイルを適用したい場合は::slotted()疑似要素を使います。これはshadow DOM内に配置されたCSSの中で使われた時のみ機能します。

上記は先ほどのソースの実行例です。slot要素で挿入された.slotted_headに対してcolor: red;が適用されているのでこのような出力になります。

独自の要素を作る カスタム要素

カスタム要素とは、オリジナルの要素ツクールです。

<my-original-tag>
<p>こんな風にmy-original-tagという要素を作ることができる</p>
</my-original-tag>

こんな感じ。任意の名称・任意の機能を持つ要素を自分で作れるよ、ということです。

まあなんでもかんでも好き放題名前をつけてよいというわけでなく、一応命名のルールがあります。アンダースコア(_)やハイフン(-)も使えるので、常識的なネーミングしてればそんな気にすることはないと思いますが、大文字は使っちゃダメらしいので注意。

こんな感じで使います。

これが実際のブラウザ上では

ブラウザ上での表示例

こんな感じになります(上記はGIFアニメでの再現例)。これでいつでもどこでもゲーミング「Hello World!」を表示してくれるカスタム要素<hello-world>を作ることに成功しました。

カスタム要素と意味論

ところで、カスタム要素で<button-original>というものを作ったとしましょう。これは名前の通りボタンとしての機能を持っていて、button要素と同様の振る舞いをするカスタム要素です。

しかし、カスタム要素はブラウザ上では(div要素と同じように)セマンティック的な意味を持たない要素として解釈されます。「ボタンとして使う意図を持って作ったカスタム要素なのに、ブラウザからはボタンとして認識されていない」ってのは何かと困ると思います。

具体的に言うと、キーボード操作できないとかスクリーンリーダー(音声読み上げソフト)で読み上げないとか、そういった諸々の弊害があります。こうした問題を解決すべく誕生した仕様がWAI-ARIA(ウェイ アリア)です。

これ自体はWeb Componentsと関係ないんでここではあまり深堀りしませんが(=IEでも動く)、WAI-ARIAというのはアクセシビリティを担保するための属性の仕様になります。 具体的には、要素の役割を示すrole属性(role="〇〇")と、要素の性質や状態を示すaria属性(aria-**="〇〇")に関する内容です。

<button-original role="button" tabindex="0" aria-pressed="false">

button-originalにWAI-ARIAを活用した例が上記になります。roleで「これはボタンです」という役割を示して、aria-pressedでボタンが押されているか否かを設定(押された時だけJSでtrueに変更する)、ついでにtabindex=”0″でキーボード操作時にフォーカスが当たるようにする、といった具合です。

カスタム要素にアクセシビリティ的な配慮が必要になる場合はこのWAI-ARIAを活用することになるので、覚えておきましょう。細かい使い方とかは今回は省くので適宜調べてください。

いつかアクセシビリティ関連の記事も書こうと思ってるので(いつになるか分からんけど…)今回は雑な紹介で勘弁してくれ。

JS上で別のJSファイルを読み込む ESモジュール

必ずしもWeb Componentsだけに関係深い技術というわけではないのですが、歴史的経緯でこのESモジュールという技術もWeb Componentsに含まれることがあります。

HTML上で別のHTMLファイルを読み込めるHTML Importsという技術があり、これもWeb Componentsを代表する機能の一つとして扱われていたんですが、「それESモジュールでもできるくね?」ってなってHTML Importsの方は没になりました。

で、このESモジュールというのは、一言でいうと「他のJSファイルやその記述をJS上でインポートする仕組み」になります。Webpack等モジュールバンドラーの知識がある方はイメージしやすいかもしれませんね。ただJSをインポートするだけならNode.jsのインストールだの環境構築だのをガチャガチャやらなくても問題なくなりましたよというお話でございます。

使い方は極めてシンプル。トップレベルに下記のように書けばよいです。

import ‘/path/**.js’;

JSファイルAではShadow DOMの大枠だけ作っておいて、ファイルAにインポートするJSファイルBではAで使用しているカスタム要素の記述を書く…みたいな対応が可能です。

なお、上記の例だと**.jsは即時読み込み&実行されますが、任意のタイミングでファイルを読み込みたいという場合は下記のように動的インポートを使います。

// 非同期処理なので、consoleが表示されてから**.jsの内容が表示される import(‘/path/**.js’);
console.log(‘console’); /*
通常のimport文(import ‘/path/**.js’;)の場合、
**.jsの内容が実行されてからconsoleが表示されるという違いがある
*/

// 逆に、import()で**.js読み込み後にconsoleを表示させる場合は下記のように書く import(‘/path/**.js’).then(module => {
  console.log(‘console’);
});

参考:
Web Components の最近の仕様と開発手法
全モダンブラウザで使えるJavaScriptのdynamic import(動的読み込み)

おわりに

エアプなりになるべく分かりやすく読めるよう努力しましたが、学問なき経験は、経験なき学問に勝るなんてイギリスのことわざにもあるように、結局使ってみないとどんなもんかピンと来ないと思います。

ま、そういうわけなので…ちゃんと覚えたい方はとにかく書いて慣れるのをオススメします(丸投げ)。あとは書籍買うとかね。

おまけ:iOS Safariのバグ

Can I use…曰く

Certain CSS selectors do not work (:host > .local-child) and styling slotted content (::slotted) is buggy.

  • :host > .(子要素名) {} の指定が機能しない

  • ::slotted()にバグがある

iOS Safariには上記の問題があるらしいです。そのうち直ることを期待しましょう。::slottedのバグが具体的にどのようなものについてかは調べてもパッと出てこなかったので気が向いたら加筆します。