Reactの代わりにWeb Componentsを使おうとした時の所感


こんにちは! エンジニアの きみどりはるか です!
Web Componentsを触ってみたので報告するね!

まとめ

  • HTML Templateは現状ちょっと使えない
  • JavaScriptファイルにHTMLも書き込んでコンポーネント化するのが良さそう
  • Reactとの組み合わせに期待

まずWeb Componentsの紹介

概要(公式翻訳+要約)

  • 新しいカスタム、再利用可能な、カプセル化されたHTMLタグを作成することができるウェブプラットフォームAPIのセット
  • 既存のWeb標準がベース

4つの特徴

  • Custom Elements
  • Shadow DOM
  • ES Modules
  • HTML Template

具体的な説明はこちらを参照
WEBCOMPONENTS.ORG

ブラウザサポート

もっとも対応が進んでいないCustom Elementsを代表してみてみよう
2022年1月現在は以下のような感じ
Safariで完全対応していないけど、Chromium系だと対応している

Try① HTML Templateを使ったコンポーネント

チュートリアル的に作ろうと思えば、HTML Templateとslotを用いた方法でコンポーネントを作ることになるかも。

HTML
<template id="hello-web-components">
  <style>
    slot[name="hello-text"] {
      text-align: center;
      font-weight: bold;
    }
  </style>

  <slot name="hello-text"></slot>
</template>

<hello-web-components>
  <h1 slot="hello-text">こんにちわ xxxxさん</h1>
</hello-web-components>
JavaScript
class HelloWebComponents extends HTMLElement {
  // 要素がドキュメントに挿入された時にコールされる
  connectedCallback() {
    const 
      shadow = this.attachShadow({ mode: 'closed' }),
      template = document.getElementById('hello-web-components').content.cloneNode(true)

    shadow.append( template );
  }
}

// カスタム要素の登録
customElements.define('hello-web-components', HelloWebComponents);

HTMLの方は<template></template>とCustom Elementの<hello-web-components></hello-web-components>から成っている。
JavaScriptの方のコードは<hello-web-components>を定義している。
少しややこしいが、Custom Elementの定義の際に<template>...</template>を使用していて、出来上がったCustom ElementをHTMLで使用するという関係になっている。
私は上記のことを完全に理解すると、さっそくコンポーネントの作成に取り掛かった。

難しい問題に行き当たる

さっそくテキストフォームのコンポーネントを作成しようとし、componentsディレクトリ直下に、text-form.htmlファイルを作成

project_root/
├── index.html
├── components/
│   └── text-form.html
└── src/

components配下には他にもたくさんコンポーネントを作っていって、index.htmlで読み込んで使っていけばよさそうだ
text-form.htmlの中身はこんな感じを考えた

text-form.html
<template id="text-form">
  <style>
    ...
  </style>

  <div id="ok-label">OK</div>
  <div id="cancel-label">Cancel</div>
  <input type="text" id="input" placeholder="入力してください" />

  <script>
    const input = document.getElementById('input');
    input.addEventListener('blur', (e) => {
      ...      
    }
  </script>
</template>

<script>
class TextForm extends HTMLElement {
   ...(上記のテンプレートを使ってカスタム要素を作る処理)...
}

customElements.define('text-form', TextForm)
</script>

ここまでやって、コンポーネントをindex.htmlにインポートする手段が無いことに気づいた
実はWeb ComponentsにはHTML Importsという、htmlファイルを他のhtmlファイルにインポートする仕組みがあった。
しかし紆余曲折あり、その機能は提供されなくなった。
そうなるともう、このHTMLコンポーネントをどう取り込めばいいのか頭を悩ませることになった。
先人の中には、webpackのhtml-loader等でバンドルしている方もいたが、それだと使用しないモジュールも含めてバンドルすることとなり、実用的でない気がする。
一旦この方式は諦める

Try② JavaScriptにHTMLも書いちゃう

HTML Templateを諦めれば、Custom ElementsとShadow DOMを利用してコンポーネントを作成していくことが可能。
すなわち、こういう感じにしてしまうこと(百聞は一見にしかず)

text-form.js
class TextForm extends HTMLElement {
  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = this.template();
  }

  template() {
    return `
      <div id="ok-label">OK</div>
      <div id="cancel-label">Cancel</div>
      <input type="text" id="input" placeholder="入力してください" />

      <script>
        const input = document.getElementById('input');
        input.addEventListener('blur', (e) => {
          ...
        }
      </script>
    `;
  }
}

customElements.define('text-form', TextForm);

ご覧のようにCustom Elementの定義の中にHTMLを書き込んでいる
こうすればtext-form.jsをimportすることで<text-form>要素を使用することができるようになる

この方式の欠点

今のところ、vs codeのシンタックスハイライトが対応してなくて、HTMLがめちゃくちゃ書きにくいぐらいしかない
もっと作っていくと色々見えてくるかもしれない

webcomponents.orgで公開されているコンポーネントはほぼTry②を採用

webcomponents.orgには有志がWeb Componentsで便利コンポーネントを作成し、公開しておられます。
もちろん全数調査したわけではありませんが、私の観測範囲ではすべてTry②方式でした。
バンドラーを前提としない形での配布になるのは、あたりまえっちゃ当たり前かもしれませんが…

感想

正直Reactは使った方がいいなと思いました
ただ、ReactやVueなどのフレームワークとShadow DOMを組み合わせるとかなり良さそうなので、次回はそっちを試してみたいです
以上!きみでりはるかでした!