Web Componentsをマスターする:フレームワーク疲れに陥らずに再利用可能なUIを構築する

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

フレームワーク依存による高いコスト

フロントエンド開発は、絶え間ないアップグレードの繰り返しのようです。ある年はチームでReactを採用し、翌年には新しいプロジェクトでVueやSvelteが必要になることもあります。これらのエコシステムは強力な抽象化を提供しますが、一方でベンダーロックインを招きがちです。Reactで堅牢なUIライブラリの構築に6ヶ月費やしたとしても、会社がAngularを使用しているチームを買収すれば、そのコードは事実上使い物にならなくなってしまいます。

Web Componentsはこのサイクルを断ち切ります。これは、カプセル化されたカスタムHTMLタグを作成できるネイティブブラウザAPIの集合体です。特定のライブラリではなくW3C標準に依存しているため、世界中で使用されているブラウザの97%でネイティブに動作します。筆者は、異なる部門が全く異なる技術スタックを使いながらも、同じ基盤となるUIロジックを共有しているエンタープライズチームにおいて、このアプローチがデザインシステムを安定させる様子を目の当たりにしてきました。

ネイティブコンポーネントを支える3つの柱

フレームワークに依存しないコンポーネントの構築は、3つの主要技術に基づいています。これらは複雑な抽象化ではなく、毎日使用しているHTMLとJavaScriptの直接的な拡張です。

1. Custom Elements(カスタム要素)

Custom Elementsを使用すると、独自の動作を持つHTMLタグを定義できます。プロフィールカードを作成するために5つの<div>タグをネストする代わりに、単に<user-card>を使用できます。ブラウザはこのタグを、ネイティブの<button><section>と同じ優先順位で扱います。

2. Shadow DOM

カプセル化はCSSにおける最大のハードルです。標準的なドキュメントでは、たった一つのp { color: red; }というルールがレイアウト全体を台無しにすることがあります。Shadow DOMは、要素に孤立したDOMツリーを付加することでこれを解決します。この「シャドウ」内部で定義されたスタイルが外部に漏れることはなく、グローバルなスタイルが内部に侵入することもありません。これは、コンポーネントの内部構造に対して真のプライベートスコープを提供します。

3. HTML Templates and Slots(テンプレートとスロット)

<template><slot>要素を使用すると、呼び出されるまでレンダリングされないマークアップを定義できます。スロットは動的コンテンツのプレースホルダーとして機能します。これらはReactのprops.childrenによく似ており、定義済みの構造にカスタムテキストや要素を渡すことができます。

実践演習:プロフィールカードの構築

自己完結型のユーザープロフィールカードを構築してみましょう。このコンポーネントは、異なるデータを受け取れる柔軟性を保ちつつ、自身のレイアウトとスタイリングを処理します。

ステップ1:テンプレートの定義

まずは構造とスタイルを定義します。ここでCSS変数(Custom Properties)を使用するのは賢い選択です。外部の開発者が内部構造を壊すことなく、コンポーネントのテーマを変更できるようになるからです。

<template id="user-card-template">
  <style>
    :host {
      display: block;
      font-family: system-ui, sans-serif;
      background: #ffffff;
      width: 280px;
      border-radius: 12px;
      box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
      border: 1px solid #e5e7eb;
      margin: 1rem;
    }
    .container {
      padding: 24px;
      text-align: center;
    }
    img {
      width: 96px;
      height: 96px;
      border-radius: 9999px;
      object-fit: cover;
      border: 4px solid #f3f4f6;
    }
    h3 {
      margin: 16px 0 4px;
      color: #111827;
    }
    p {
      color: #6b7280;
      font-size: 0.875rem;
      margin-bottom: 16px;
    }
    button {
      background: #2563eb;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      font-weight: 500;
      cursor: pointer;
    }
    button:hover {
      background: #1d4ed8;
    }
  </style>
  <div class="container">
    <img src="" alt="ユーザーアバター" id="avatar" />
    <h3><slot name="username">匿名ユーザー</slot></h3>
    <p><slot name="role">コントリビューター</slot></p>
    <button id="toggle-info">プロフィールを表示</button>
  </div>
</template>

ステップ2:コンポーネントロジックの作成

次に、HTMLElementを継承したJavaScriptクラスを作成します。ここでShadow DOMをアタッチし、コンポーネントのライフサイクルを処理します。

class UserCard extends HTMLElement {
  constructor() {
    super();
    // シャドウルートを初期化
    this.attachShadow({ mode: 'open' });

    const template = document.getElementById('user-card-template');
    const content = template.content.cloneNode(true);
    this.shadowRoot.appendChild(content);
  }

  connectedCallback() {
    // 属性またはデフォルトから画像ソースを設定
    const avatarUrl = this.getAttribute('avatar') || 'https://i.pravatar.cc/150?u=default';
    this.shadowRoot.querySelector('#avatar').src = avatarUrl;

    this.shadowRoot.querySelector('#toggle-info').addEventListener('click', () => {
      const name = this.querySelector('[slot="username"]')?.innerText || 'ユーザー';
      console.log(`${name}のプロフィールに移動中...`);
    });
  }
}

// ブラウザに要素を登録
customElements.define('user-card', UserCard);

ステップ3:実装

コンポーネントの使用は、標準的なHTMLを書くのと同じくらい簡単です。これをReactアプリ、WordPressサイト、あるいは単純なindex.htmlファイルに組み込むことができます。

<!-- ネイティブでの使用 -->
<user-card avatar="https://i.pravatar.cc/150?u=1">
  <span slot="username">Alex Rivera</span>
  <span slot="role">リードアーキテクト</span>
</user-card>

<user-card avatar="https://i.pravatar.cc/150?u=2">
  <span slot="username">Sarah Chen</span>
  <span slot="role">UXデザイナー</span>
</user-card>

戦略的なメリット

このワークフローに欠けているものに注目してください。npm installも、Webpackの設定も、30KBものランタイムライブラリも必要ありません。レンダリングとカプセル化という重い処理は、ブラウザが担当します。大規模なデザインシステムの場合、ネイティブコンポーネントに切り替えることで、Reactベースのライブラリと比較して初期のJSバンドルサイズを40〜60KB削減できる可能性があります。

相互運用性(インターオペラビリティ)こそが、ここでの真の利点です。もし組織が来期にReactからVueに移行したとしても、作成した<user-card>はそのまま使えます。スクリプトをインポートして、タグを使い続けるだけです。これにより、JavaScriptエコシステムの避けられない変化に対して、UIを将来にわたって保護(フューチャープルーフ)することができます。

重要なトレードオフ

Web Componentsは強力ですが、あらゆるタスクに最適なツールというわけではありません。以下の点を考慮してください。

  • SEO: GooglebotはShadow DOMを効果的にレンダリングしますが、すべての検索エンジンに対して最大限の可視性を確保するためには、重要なテキストコンテンツは依然として「Light DOM」(スロット内)に配置すべきです。
  • テーマ設定: スタイルがカプセル化されているため、グローバルなCSSファイルから単に上書きすることはできません。カスタマイズ用の特定の「フック」を公開するには、CSS Parts(::part)やCSS変数を使用する必要があります。
  • 状態管理: データのネストが深い複雑なアプリケーションでは、Litのような軽量ライブラリの使用を検討するとよいでしょう。フルフレームワークのようなオーバーヘッドなしに、宣言的な方法で更新を処理できます。

次のステップ

今すぐスタック全体を書き直さなければならないと焦る必要はありません。まずは、ローディングスピナーやカスタムトグルスイッチなど、小さく繰り返しの多いUI要素から始めてみましょう。それをネイティブのWeb Componentとして構築してみてください。互換性の問題を一切気にすることなく、同じコンポーネントを3つの異なるプロジェクトに組み込める手軽さを一度体験すれば、ウェブプラットフォーム向けに構築することの価値は否定できないものになるはずです。

Share: