🐏

Web Components を実装して理解する

2024/10/18に公開

Web Components とは

Web Components は、再利用可能なカプセル化された オリジナルの HTML タグを作成できる技術です。
Web 標準の技術で、モダンブラウザではサポート済みの技術となっています。
この技術を使うことで、React や Vue など特定のフレームワークに依存しない標準化されたコンポーネントを作成することができます。

Web Components を理解する上で主要な技術は以下の3つです。

カスタム要素

独自の HTML タグを定義し、再利用可能なコンポーネントを作成するための仕組みです。
これにより、シンプルで直感的なタグとして扱うことができます。
カスタム要素の名前には、ハイフンを1つ以上含む必要があります。
(例)<hello-world></hello-world>

シャドウ DOM

カスタム要素内の要素やスタイルをカプセル化するための仕組みです。
これにより、 HTML や CSS が他の要素に影響を与えたり、逆に他の要素から影響を受けたりすることがなくなります。

HTML テンプレート

template タグを使用して、非表示のHTML構造を定義し、動的に再利用できる仕組みです。
また、 slot タグを組み合わせることで、テンプレート内に柔軟なコンテンツの差し込みが可能になります。

実装してみる

実際に実装してみます。

最小の実装

まずは、 Hello, World! と表示するだけの簡単なコンポーネントから実装してみます。
main.js に以下を記述します。

class HelloWorld extends HTMLElement {
  constructor() {
    super();
    // シャドウルートを作成
    // mode: "closed" で JavaScript からシャドウルートにアクセスできないようにできる(アクセス可は open)
    const shadow = this.attachShadow({ mode: "closed" });
    // シャドウルートにHTMLを挿入
    shadow.innerHTML = `<p>Hello, World!</p>`;
  }
}

// カスタム要素を登録
customElements.define("hello-world", HelloWorld);

index.html にカスタム要素 <hello-world> タグを設置し、用意した main.js を読み込みます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebComponents</title>
  </head>
  <body>
    <!-- カスタム要素の設置 -->
    <hello-world></hello-world>
  </body>
  <script src="./main.js"></script>
</html>

ブラウザで表示すると hello-world コンポーネントが表示できていることが確認できます。

スタイルの適用

もちろんスタイルを適用することも可能です。
以下のように main.js を変更してスタイルを適用します。

class HelloWorld extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: "closed" });
    shadow.innerHTML = `
    <p>Hello, World!</p>
    <!-- スタイルを追加 -->
    <style>
      p {
        color: red;
        background-color: pink;
      }
    </style>
    `;
  }
}

customElements.define("hello-world", HelloWorld);

表示確認すると、文字色と背景色が変わっています。

slot

次に<slot> タグを使って表示する要素を index.html で指定できるように実装してみます。

main.js の Hello, World! の文字を <slot> タグに置き換えます。

class HelloWorld extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: "closed" });
    shadow.innerHTML = `
    <!-- slot に置き換えて index.html 側で指定できるようにする -->
    <p><slot></slot></p>
    <style>
      p {
        color: red;
        background-color: pink;
      }
    </style>
    `;
  }
}

customElements.define("hello-world", HelloWorld);

index.html で <hello-world>タグ内に slot に Hello, Kitty! の文字を設定します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebComponents</title>
  </head>
  <body>
    // slot に Hello, Kitty! と表示する
    <hello-world>Hello, Kitty!</hello-world>
  </body>
  <script src="./main.js"></script>
</html>

適切に slot に Hello, Kitty! の文字が反映されました。

template

template タグを使うと html ファイルにテンプレートを書いておくことも可能なので、そちらも試してみます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebComponents</title>
  </head>
  <body>
    <!-- テンプレートを定義 -->
    <template id="hello-template">
      <p><slot></slot></p>
      <style>
        p {
          color: blue;
          background-color: lightgray;
        }
      </style>
    </template>

    <!-- カスタム要素の設置 -->
    <hello-world>Hello, Template!</hello-world>
  </body>
  <script src="./main.js"></script>
</html>
class HelloWorld extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: "closed" });

    // テンプレートを取得
    const template = document.getElementById("hello-template");
    const templateContent = template.content.cloneNode(true);

    // シャドウルートにテンプレートを挿入
    shadow.appendChild(templateContent);
  }
}

customElements.define("hello-world", HelloWorld);

取得したテンプレートがカスタム要素に反映されました。

Declarative Shadow DOM

従来の Web Components の課題

先ほど説明した実装例のように Web Components の作成には、従来 JavaScript が必要でした。
そのため、 Web ページのレンダリング → JavaScript の実行 → Web Components が描画 という流れになるため、 SEO と SSR に対応することが難しかったり、レイアウトシフトよるチラつきが発生するという問題がありました。

課題への対応策

ここで登場した技術が、 Declarative Shadow DOM です。
Web Components で使用する シャドウ DOM を HTML 内で宣言的に定義するための仕組みです。
これを利用することで SEO と SSR への対応やレイアウトシフトを起こさない画面描画を実現することができます。

こちらの技術も最近、主要なモダンブラウザの新しいバージョンでは対応済みとなっています。

実装してみる

実装は簡単で、定義したカスタム要素内の直下に template タグを設置し、 shadowrootmode 属性を設定するだけです。

<hello-world>
 <!-- shadowrootmode closed で JavaScript からシャドウルートにアクセスできないようにできる(アクセス可は open) -->
  <template shadowrootmode="closed">
    <style>
      p {
        color: red;
        background-color: pink;
      }
    </style>
    <p>Hello, World!</p>
  </template>
</hello-world>

JS で実装したときと同じく、Web Components として表示できました。

Declarative Shadow DOM の課題?

少し調査してみましたが、どうやら JavaScript で定義したときのように同一の要素やスタイルをカスタム要素に紐づけて再利用することは難しそうです。
これだと今のところ Web Components の恩恵を殆ど受けられないので使うメリットは薄いのではないかという印象を受けました。
もし、私の調査不足で再利用する方法があるようでしたら、コメントで教えていただけますと幸いです。

まとめ

Web Components を使うことで再利用可能なフレームワーク非依存のUIコンポーネントを作成することができました。
Declarative Shadow DOM という新たな技術も登場し、これまで課題だった SEO や SSR、レイアウトシフトに対する対応も進んできていることが分かりました。
進化中の技術であるため、今後の Web Components の動向にも注目していきたいです。

参考

Discussion