shibomb

Web ComponentsでUI資産を構築:フレームワークを超え再利用可能にする

React や Vue.js といったフレームワークに習熟する中で、「このUIコンポーネント、別のフレームワークを使っているプロジェクトでも使い回せたら便利なのに…」と感じたことはありませんか?特定の技術に依存せず、Webサイトやアプリケーションのどこでも再利用できる部品を作りたいというニーズは、開発現場で常に存在します。この記事では、そんな課題を解決する Web標準 の技術「Web Components」に焦点を当てます。Web Components の基本から、実践的な設計・実装方法、そしてチーム開発で活かすためのヒントまで、手を動かしながら学んでいきましょう。

なぜ今、Web Componentsが重要なのか?フレームワーク全盛期におけるWeb標準の価値

現代のWeb開発は、React、Vue、Angularをはじめとするコンポーネントベースのフレームワークが主流です。これらのフレームワークは生産性を劇的に向上させましたが、一方で「フレームワークへのロックイン」という課題も生み出しました。あるフレームワークで作ったコンポーネントは、原則としてそのフレームワークの中でしか利用できません。

ここで輝きを放つのが Web Components です。Web Components は、特定のライブラリやフレームワークに依存しない、ブラウザ自身がネイティブに理解できるコンポーネント作成技術です。これは W3C という標準化団体によって定められた Web標準 の技術群であり、主要なモダンブラウザ(Chrome, Firefox, Safari, Edge)でサポートされています。

Web Components の最大の価値は、その 相互運用性将来性 にあります。 一度作成したコンポーネントは、React プロジェクト、Vue プロジェクト、あるいはフレームワークを一切使わない静的なHTMLページでも、同じように動作します。これにより、企業のデザインシステムを構築する際に、技術スタックの異なる複数のチームで共通のUI部品ライブラリを共有することが可能になります。また、Web標準に基づいているため、特定のフレームワークの流行り廃りに影響されにくく、長期的な資産としてコンポーネントを維持できるのです。

Web Componentsを構成する3つの要素:カスタム要素、Shadow DOM、HTML Templatesの基本を理解する

Web Components は単一の技術ではなく、以下の3つの主要な技術を組み合わせて実現されます。これらが連携することで、自己完結型で 再利用可能なUI コンポーネントが生まれます。

  1. カスタム要素 (Custom Elements)
    <div><p> といった既存のHTMLタグのように、自分で独自のHTMLタグを定義し、その挙動をJavaScriptで制御するためのAPIです。例えば、<my-profile-card><emoji-picker> といった、意味的で分かりやすい名前の要素を作成できます。これにより、HTMLのマークアップがより直感的になります。

  2. Shadow DOM
    コンポーネントの内部構造(HTMLマークアップ、CSSスタイル、JavaScript)を、ページの他の部分から完全に隔離するための仕組みです。Shadow DOM を使うと、コンポーネントのスタイルが意図せず外部に影響を与えたり(CSSの漏洩)、外部のスタイルによってコンポーネントの見た目が壊れたりするのを防げます。これこそが、Web Components のカプセル化を実現する心臓部です。

  3. HTML Templates (<template><slot>)
    ページの読み込み時にはレンダリングされないHTMLの断片を定義するための仕組みです。<template> タグでコンポーネントの基本的な骨格を定義しておき、JavaScriptを使って必要な時にその内容を複製して利用します。また、<slot> タグを使うことで、コンポーネントの利用者が外部から独自のコンテンツを挿入するための「差し込み口」を提供できます。

これらの技術を組み合わせることで、まるでブラウザに組み込まれたネイティブ要素のように振る舞う、堅牢で再利用性の高いコンポーネントを作成できるのです。

手を動かして実感!シンプルなカスタム要素を作成してみよう(サンプルコード付き)

百聞は一見に如かず。まずは最も基本的なカスタム要素を作って、Web Components の世界に触れてみましょう。ここでは、「こんにちは」と表示するだけのシンプルな <hello-world> タグを作成します。

まず、プロジェクトフォルダに index.htmlhello-world.js という2つのファイルを用意してください。

index.html には、以下のように記述します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>カスタム要素のテスト</title>
</head>
<body>
  <h1>Web Componentsをはじめよう!</h1>
  <hello-world></hello-world>
  
  <script type="module" src="./hello-world.js"></script>
</body>
</html>

次に、hello-world.js にカスタム要素の定義を記述します。

// HTMLElementを継承したクラスを定義する
class HelloWorld extends HTMLElement {
  constructor() {
    // 親クラスのコンストラクタを必ず最初に呼び出す
    super(); 
    
    // この要素が生成されたときの処理
    this.textContent = 'こんにちは、Web Components!';
  }
}

// 'hello-world' というタグ名で、今作ったHelloWorldクラスをカスタム要素として登録する
customElements.define('hello-world', HelloWorld);

この index.html をブラウザで開くと、画面に「こんにちは、Web Components!」と表示されるはずです。

やっていることは非常にシンプルです。

  1. HTMLElement という、すべてのHTML要素の元となるクラスを継承して、独自のクラス (HelloWorld) を作ります。
  2. constructor の中で、要素が作られたときの初期処理を記述します。ここでは、要素自身のテキストを書き換えているだけです。
  3. customElements.define() メソッドを使って、ブラウザに「<hello-world> というタグを見つけたら、HelloWorld クラスのインスタンスとして扱ってください」と教えます。

これがカスタム要素の第一歩です。ただし、この状態ではまだスタイルがカプセル化されていないため、ページのグローバルなCSSの影響を直接受けてしまいます。次に、この問題を解決していきましょう。

実践的なUIコンポーネント設計:Shadow DOMでスタイルと挙動を完全にカプセル化する

カスタム要素の真価は Shadow DOM と組み合わせることで発揮されます。Shadow DOM を使うと、コンポーネントの内部実装を外部から隠蔽し、独立した存在として振る舞わせることができます。

先ほどの hello-world.js を、Shadow DOM を使うように書き換えてみましょう。

class HelloWorld extends HTMLElement {
  constructor() {
    super();

    // この要素にShadow DOMをアタッチする
    const shadow = this.attachShadow({ mode: 'open' });

    // Shadow DOM内に適用するスタイルを定義
    const style = document.createElement('style');
    style.textContent = `
      p {
        font-family: sans-serif;
        font-weight: bold;
        color: tomato;
        border: 2px solid #ccc;
        padding: 1rem;
        border-radius: 8px;
      }
    `;

    // Shadow DOM内に表示する要素を定義
    const p = document.createElement('p');
    p.textContent = 'こんにちは、カプセル化された世界!';

    // 作成したスタイルと要素をShadow DOMに追加
    shadow.appendChild(style);
    shadow.appendChild(p);
  }
}

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

index.html は変更せずにブラウザをリロードしてみてください。今度は、トマト色の文字に枠線が付いたスタイルで表示されるはずです。

ここでの重要なポイントは this.attachShadow({ mode: 'open' }) です。これにより、このカスタム要素に「シャドウツリー」と呼ばれる、外部から隔離されたDOMツリーが接続されます。そして、その中に <style> タグを配置することで、ここで定義されたスタイル (p 要素に対するスタイル) は、このコンポーネントの 内部にのみ 適用されるようになります。

試しに index.html<p>これは外の世界のPタグです</p> と追記しても、このPタグの文字はトマト色にはなりません。逆に、グローバルなCSSで p { color: blue; } と定義しても、コンポーネント内部の文字色は青に変わりません。このように、スタイルと構造を完全にカプセル化できるのが Shadow DOM の強力な機能です。

外部との連携を設計する:イベントリスナーと属性・プロパティを活用したデータフロー

自己完結しているコンポーネントも、外部のアプリケーションと情報をやり取りする必要があります。Web Components では、この連携を以下の標準的な方法で行います。

  • 親から子へ(データの入力): 属性 (attributes) と プロパティ (properties) を使う
  • 子から親へ(イベントの通知): カスタムイベント (Custom Events) を使う

これは、多くのモダンフレームワークで採用されているデータフローの考え方とよく似ています。具体的なカウンターコンポーネントの例で見てみましょう。

// <click-counter> コンポーネント
class ClickCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
    
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 8px 16px;
          border-radius: 4px;
          cursor: pointer;
        }
      </style>
      <button>Clicked: 0 times</button>
    `;

    this.button = this.shadowRoot.querySelector('button');
  }
  
  // 要素がDOMに追加されたときに呼ばれるライフサイクルコールバック
  connectedCallback() {
    this.button.addEventListener('click', () => {
      this.count++;
      this.button.textContent = `Clicked: ${this.count} times`;
      
      // 'change' という名前のカスタムイベントを発行する
      this.dispatchEvent(new CustomEvent('change', {
        detail: { count: this.count } // イベントに含めるデータ
      }));
    });
  }
}

customElements.define('click-counter', ClickCounter);

このコンポーネントは、ボタンがクリックされるたびに内部のカウンターを増やし、同時に change という名前のカスタムイベントを発行します。外部のJavaScriptは、このイベントを監視することで、コンポーネントの状態変化を検知できます。

<click-counter id="my-counter"></click-counter>
<p>カウンターの値: <span id="output">0</span></p>

<script type="module" src="./click-counter.js"></script>
<script>
  const counter = document.getElementById('my-counter');
  const output = document.getElementById('output');
  
  // カスタムイベント 'change' をリッスンする
  counter.addEventListener('change', (event) => {
    // event.detail にコンポーネントから渡されたデータが入っている
    output.textContent = event.detail.count;
  });
</script>

このように、属性で初期値を渡し、プロパティで動的に状態を操作し、カスタムイベントで内部の変化を外部に通知する、という明確なインターフェースを設計することが、再利用性の高いコンポーネントを作る上での鍵となります。

Web Componentsをチーム開発で活かす:設計原則とメンテナンス、そして既存フレームワークとの共存術

Web Components をチームで効果的に活用するためには、いくつかの設計原則とプラクティスを共有することが重要です。

まず、コンポーネントは 単一責任の原則 に従い、一つのことだけをうまくやるように設計しましょう。ボタン、入力フォーム、モーダルダイアログなど、機能を明確に分割することで、組み合わせやすく、メンテナンスしやすいコンポーネントになります。

また、Storybook のようなツールを使ってコンポーネントのカタログを作成することも非常に有効です。これにより、チームメンバーはどんなコンポーネントが利用可能かを一覧でき、様々な状態でコンポーネントの見た目や挙動をインタラクティブに確認できます。これは、開発者とデザイナーのコミュニケーションを円滑にする上でも役立ちます。

そして最も重要なのが、既存フレームワークとの共存 です。幸いなことに、現在の主要なフレームワーク (React, Vue, Angular など) は、Web Components (カスタム要素) を特別な設定なし、あるいは簡単な設定でそのまま利用できます。例えば、React の JSX の中で <my-profile-card> のようにカスタム要素を記述すれば、問題なくレンダリングされます。これにより、アプリケーションのコア部分は使い慣れたフレームワークで開発しつつ、デザインシステムなどの共通部品は Web Components で構築し、複数のプロジェクトで共有する、といったハイブリッドな構成が可能になります。

Web Components は、特定のフレームワークを置き換えるものではありません。むしろ、それらと共存し、Web開発のエコシステム全体をより堅牢で相互運用可能なものにするための、強力な「標準技術」という選択肢なのです。ぜひあなたの次のプロジェクトで、このフレームワークに依存しないコンポーネント開発の世界を試してみてください。

関連記事