[再入門] WebComponentsは何を実現するのか?

TopPage 出典: https://seeklogo.com/

初めに

以前から存在し、ネーミングからモダンフロントの近未来を感じさせるWebComponentsに関して、基本的な技術の概観と2021年現在に考えて何を解決してくれる技術なのかを調査してみました。

概要

WebComponentsとはでよくある定義が、Web標準技術を利用して再利用可能なUIコンポーネントを作成する技術で、以下の3つの技術から構成されます。

  • カスタム要素
  • Shadow DOM
  • HTML テンプレート

イメージとして WebComponents=カスタム要素 と思っている方も多いと思いますが、単なる要素としてではなく、コンポーネントとして再利用可能なUIを定義するために必要な機能が補完される物と考えています。

要素技術の解説

3つの要素技術に関して、それぞれ概観を掴んで行きたいと思います。

カスタム要素

独自のHTMLタグを定義する技術です。既存のHTMLタグ(a, pタグなど)を拡張することもできます。カスタム要素の定義には以下様にwindow.customElements.defineを利用します。

ex) カスタムのタイトル要素

<my-element1></my-element1>
<script>
  class MyElement1 extends HTMLElement {
    constructor() { super(); }

    connectedCallback() {
      const title = document.createElement('h1');
      title.textContent = 'タイトル';
      this.appendChild(title);
    }
  }
  customElements.define('my-element1', MyElement1);
</script>

render result

カスタムコンポーネントの命名には-を必ず利用する縛りになっており、標準要素と識別されています。

現在の定義方法はv1で、v0ではdocument.registerElementを利用していたのが、今はdeprecatedになっています。

Shadow DOM

Shadow DomはコンポーネントをCSS, JS的に隔離するための機能です。APIとしてはattachShadowを利用します。

<p>外側</p>
<style>p {color: blue;}</style>

<div id="shadowHost"></div>
<script>
  const shadowHost = document.getElementById('shadowHost');
  const shadow = shadowHost.attachShadow({mode: "open"});

  const title = document.createElement('p');
  title.textContent = 'Shadow Dom 内';
  const style = document.createElement('style');
  style.textContent = 'p {color: red;}';

  const container = document.createElement('div');
  container.appendChild(title);
  container.appendChild(style);

  shadow.appendChild(container);
</script>

render result

dom result

この様にshadowHostの配下にShadowRootがアタッチされています。また、Shadow Dom内外で互いにCSSが影響を与えていないことが確認できます。

Shadow Domの全体像はMDNで下記の図が説明されています。 Shadow Dom

なお、attachShadowの引数に渡している modeopenclosed があり、closedの場合は上位ノードから参照することは出来ないため、JS的にも隔離することが可能です。

HTML テンプレート

MDNでは、<template>タグ(コンテンツテンプレート要素)と、<slot> タグの機能を指しています。 <template>は再利用可能なタグを定義する機能です。そのままでは画面上に描画されませんが、要素を clone => append することで再利用することが可能です。また、Shadow Domでは <slot> を処理することができ、タグの中に記載したDOMを配置することができます。(SPAフレームワークにもよくある機能です。)

<template id="title-template">
  <h1>
    <slot name="text">デフォルトのテキスト</slot>
  </h1>
</template>

<div id="container">
  <p slot="text">スロットに代入するテキスト</p>
</div>

<script>
  const titleTemplate = document.getElementById('title-template');
  const title = titleTemplate.content.cloneNode(true);

  const shadowRoot = document.getElementById('container').attachShadow({mode: "closed"});
  shadowRoot.appendChild(title);
</script>

render dom

render result

実は <p slot="title">...</p> の部分はshadow-rootの外に出ていますが、描画される位置は <slot name="title">...</slot> の位置になっております。前者がないとデフォルトの文字が描画され、後者を消すとタイトルが表示されなくなります。

WebComponents 改めて

上記の技術を合わせてWebComponentsはこんな感じに記述できます。

<template id="title-template">
  <div>
    <slot name="title">デフォルト</slot>
    <slot name="subtitle">デフォルト</slot>
    <p>パラグラフ</p>
  </div>

  <style>
    p { color: blue; }
  </style>
</template>

<script>
  class MyTitle extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('title-template');
      const title = template.content.cloneNode(true);

      const shadowRoot = this.attachShadow({mode: "closed"});
      shadowRoot.appendChild(title);
    }
  }
  customElements.define('my-title', MyTitle);
</script>

<my-title>
  <p slot="title">タイトル</p>
  <p slot="subtitle">サブタイトル</p>
</my-title>

結果

render result

WebComponentsのコンポーネントとしての完全さ

Reactなどの既存のSPAライブラリを利用してコンポーネントを作成する場合と比べて基本的な機能の対応を確認します。

  • テンプレート
    • HTMLテンプレートで実現
  • slot
    • HTMLテンプレート + Shadow Dom で実現
  • スタイル
    • テンプレートに記載すればいいので、自動で
  • prop
    • いい感じのAPIはなさそう

Propsに関してはうまいハンドリング方法をWeb標準で対応している分けではないので、実装でカバーする必要がありそうです。 例として、こちらの記事では4つの方法が紹介されていますが、

  • constructorでgetAttributesで取得する
  • コンポーネントクラスにget/setをはやす
  • イベントを利用する
  • イベントバスを利用する

と、自前で実装するには少し面倒だなぁという感じは否めず、propをリアクティブにするにはさらに工夫が必要になります。

WebComponentsのエコシステム

上記の様な悩みを解決する様なライブラリが多数開発されている様です。例えば WEBCOMPONENTS.ORG を参照すると 便利なライブラリ群 がまとめられていたりします。

その中でも、star数で一番数が多いのがlitになります。

lit

WebComponentを簡単に作成するためのライブラリです。WebComponentsで実装されたReact?と思わせる様なシンプルな今記述でコンポーネントを作成できます。また、litを用いて作成するコンポーネントはリアクティブにできます。

export class MyElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      padding: 25px;
      color: var(--my-element-text-color, #000);
    }
  `;

  // Propとして外から値を受け取りたいステート
  @property({ type: String }) title = 'no title';

  // コンポーネント内部だけで利用するステート
  @state() counter = 0;

  __increment() {this.counter += 1;}

  // コンポーネントの描画部分
  render() {
    // html関数によって、簡単にテンプレートを定義できる
    return html`
      <h2>${this.title} Nr. ${this.counter}!</h2>
      <button @click=${this.__increment}>increment</button>
    `;
  }
}

レポジトリの構成としては、カスタムエレメントを作成する lit-element と テンプレート管理を担当する lit-html を合わせたモノレポ構成になっています。lit-htmlのhtml関数はコンポーネントの描画部分で利用でき、いわばReactでいうJSXに代替になっています。

プロパティをリアクティブにするためにどの様な実装がされているのかですが、大まかに下記の様になっており、基本的にはReactの更新の流れと同様だと思います。

  • コンポーネント定義
    • デコレータ(@propertyなど)によって、プロパティにgetter/setterを生やす
  • イベント発生
    • setterを起動
    • update que に詰める
    • 順次状態をupdate
    • html関数を実行しDOMを生成(差分で生成してくれる)
    • 結果を描画する

Catalyst

githubが開発したWebComponentsを作成するためのライブラリです。まだstar数が1kに満たないレポですが、興味深かったのは、ドキュメント冒頭の How did we get here?(経緯) です。GitHubではモダンSPAフレームワークがもたらす宣言型UIのパラダイムに乗っておらず、旧来的なselector操作、イベントデリゲーションを好んでいる様です。その様な環境でも漸進的にWebComponentsの利点を活用していくために独自開発を行ったそうです。この辺りはキーワードになってきそうな感じがします。

WebComponentsが何を解決するか?

MDNで強調されているのは以下の二つの点です。

  • 再利用性の向上(拡張性)
  • (スタイルの)カプセル化

ReactやVueを使ってもコンポーネントを開発できるのでは?という疑問が湧いてきそうですが、WebComponentsは別レイヤーの課題を解決するために進められている技術になります。

WebComponents VS SPAフレームワーク

WebComponentsとSPA技術の対比を見ていくことで、WebComponentsの真価とは何か考えて行きたいと思います。 (以下は私見です。)

まず、SPAは特定領域の描画を委譲できます。ReactやVueではマウントポイント以下の全てのDOMの管理に責任を持ち、ページの一部もしくは全体を管理します。一方でWebComponentsは特定のコンポーネントに描画に責務を持ちます。そもそもライブラリ(フレームワーク)とコンポーネントという違うレベルの話をしているので当然ではあるのですが、再利用可能なUIをコンポーネントという単位で完結できるという部分がWebComponentsの特質すべき点かと考えております。

WebComponentsはあくまでWeb標準の技術であり、単に新たなHtmlタグを提供するものと考えればどの様な技術スタックでもコンポーネント単位で組み込むことができます。例えばReactやVueアプリの中にWebComponentsを配置することは容易です。そのため、WebComponentsはネストが容易であるとも考えられます。SPAの場合は領域を担当するので、マイクロフロントエンド 的に画面を縦割り分割することはできますが、例えばReactの中でVueを使うということには一定の考慮が必要になります(多分普通はアンチパターン)。WebComponentsの場合はコンポーネントの登録さえできれば、WebComponentsの中にWebComponentsを置くことも、複数のSPAライブラリを配置することも、その逆も実現可能です。

この様な違いはやはりフレームワークとコンポーネントというレベルの違いに起因していると考えられます。フレームワークはMountから開始し、コンポーネントをまとめ上げ、StoreやRouter管理なども担当してアプリケーションとして完成させることに責務があります。WebComponentsはコンポーネントのdefineから開始し、単一のコンポーネントを提供する事にだけ責務があります。

別の観点でいうと状態管理があります。SPA技術の重要な視点としては宣言方UIの実現があり、必ずstatefulで変数バインドが利用できます。一方WebComponentsはもちろんStateを考慮しておらず、ここも両者の目指すレベルの違いの部分です。Stateを利用するには前述した lit の様な実装が必要になります。

WebComponents構築系ライブラリの利用に関して

WebComponentsを利用する場合でも大抵の場合はJSXの様に便利なテンプレートや状態バインドが欲しくなると思いますが、では lit を利用した場合、結局特定の技術基盤にロックインされてしまうのか?という部分が気になりました。これは条件によって濃淡が出る話だと思いますが、個人的には コンポーネントの開発に閉じているか が、ライブラリ選定のポイントになるかと感じました。確かにWebComponentsを利用していても、StoreやRoutingを利用してコンポーネントをまとめ上げアプリとして仕上げるSPAと同等レベルの機能があればReactとを利用するのとなんら変わらない状態になります。もちろんうまく疎結合に実装されたコンポーネントは可搬になりますが、Storeなどに依存した時点で再利用生がなくなります。一方、どのよな仕組みであれコンポーネントの提供に閉じていれば(ほとんど)ShadowDomで隔離された世界の実装だけに止まり、アプリケーション基盤の選択は自由です。つまり、litを利用した場合はコンポーネントとして特定の技術基盤に乗りつつ、アプリケーションとしては自由な状態が実現できるため、既存のSPAフレームワークと比較して、漸進的な導入が可能がしやすいというのは重要なポイントです。前述したCatalystの様にjQuery/vanillaの様なレガシーなパラダイム下でも、コンポーネントの利点を徐々に組み込んでいく事が可能になります。

その他考えた事

  • オーバーヘッドは小さそう
  • 今回Litで作成した最小限のコンポーネント: 4kB
  • Reactだと(react-dom含め)40kBほど
  • 利用可能なカスタムエレメント一覧を取得する様なメソッドはなさそうなので、Web側の使い勝手は改善の余地があるかも
  • SPAフレームワークとの相互関係
  • React => WC: いいツールはない感じ?
  • Angular => WC: ネイティブで対応している https://angular.jp/guide/elements
  • Vue => WC: 公式対応している様子 https://v3.vuejs.org/guide/web-components.html#passing-dom-properties

まとめ

2011年にGoogleが提唱してからしばらく、幾度となく元年が訪れつつ未だに道の技術でもあるWebComponentsですが、技術の概要、エコシステム、使い所などを検討して見ました。見方によれば、既存のSPAフレームワーク系が巨大で鈍足になり、より軽量な基盤が求められている昨今では、WebComponentsの漸進的な導入がしやすいという部分は大きな利点になりえ、ブラウザ対応状況も申し分ない状態が整ってきています。また、デザインシステムの一部としてうまく活用していく事で、コンポーネント単位でUIガバナンスをとっていくのにも有用かもしれません。 今後、本業の方でも活用できる場面があるのではないかと期待しています。

末筆ながら、ここまで読んで頂きありがとうございます。

参考