Web Components & Lit入門:カスタム要素を作ってみよう|東京のWEB制作会社・ホームページ制作会社|株式会社GIG

Web Components & Lit入門:カスタム要素を作ってみよう

2023-08-21 制作・開発

こんにちは! GIGエンジニアの石倉です!

フロントエンド開発において再利用可能な部品(コンポーネント)ベースで開発を行うことは、「メンテナンス性」「堅牢性」など多くの利点がある手法です。

ReactやVue.jsといったコンポーネント指向のJavaScriptライブラリはいくつかありますが、今回はWeb標準実装である「Web Components」について触れていきたいと思います。

そして、記事後半ではWeb Componentsを簡単に扱うためのライブラリとしてGoogleが開発している「Lit」についてご紹介いたします。


Web Componentsとは?

Web Componentsは、HTML要素をコンポーネント化する技術群の総称であり、またそれらを用いて作成されたコンポーネントそのものを指します。

任意のまとまりを一つの独自のHTMLタグとして定義し、HTML内で再利用することが可能です。

例として、以下のようなカウンターUIを一つのカスタム要素として定義することができます。

 <my-counter></my-counter>


Web Componentsが生まれた背景

従来「一つの巨大HTMLと全体に適用されるCSS」といった構成で開発を行う必要があり、開発者たちは以下の課題に悩まされていました。

・HTMLで同じUI要素があっても都度記述する必要がある
・CSSが全体適用されるため設計難易度が高い

また昨今ではリッチなWebサイトが増えるなど、さらにコードの肥大化は加速し、複雑性を増している状況となっています。

※「BEM」や「FLOCSS」といったCSS設計手法は、その複雑性の問題に対処するために生まれた解決策の一つです。

上記の課題に対して、Web標準で「再利用可能なUI部品」を実現しようと生まれたものがWeb Componentsです。

Web Componentsの3大要素

Web Componentsは、下記3つの主要な要素によって成り立っています。

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

既にご紹介しておりますが、任意のまとまりを再利用可能なコンポーネントとして、任意の名前の独自HTML要素として定義できます。

2. Shadow DOM(シャドウDOM)

シャドウDOM ツリーは、メイン文書のDOMとは別にレンダリングされます。そのため、CSSの適用範囲・JavaScriptのアクセス範囲をコンポーネント内に限定することできます。

3. HTMLテンプレート(HTML template)

templateタグによって任意のHTML要素群のテンプレート化することができ、slotタグによってインスタンスへの要素注入ができます。


Web Componentsでカスタム要素を作ってみよう(基本編)

ここまでざっくりとWeb Componentsに関してご説明しましたが、具体的な実装イメージを掴むために実際にカスタム要素を作ってみましょう。

今回は冒頭でも例示しました<my-counter>というカウンターUIの実装を解説します。

【define.js】

 // ①
class MyCounter extends HTMLElement {
  constructor() {
    super();


    // ②
    this.attachShadow({ mode: "open" });


    // ③
    this.shadowRoot.innerHTML = `
      <style>
        #counter {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          width: fit-content;
          padding: 20px;
          border: 1px solid #ccc;
          border-radius: 0.5rem;
          background-color: #fff;
        }
        #count {
          font-size: 3rem;
          font-weight: bold;
        }
        #countup {
          margin-top: 1rem;
          padding: 0.5rem 1rem;
          border: 1px solid #ccc;
          border-radius: 0.5rem;
          background-color: #fff;
          font-size: 1rem;
          font-weight: bold;
          cursor: pointer;
        }
      </style>


      <div id="counter">
        <span id="count">1</span>
        <button id="countup">Count Up</button>
      </div>
    `;
  }


// ④
customElements.define("my-counter", MyCounter);


上記のコードをもとに解説をしていきます。

カスタム要素はJavaScriptで定義していくのですが、まずは①としてHTMLElementクラスを継承したMyCounterクラスを作成していきましょう。

カスタム要素のクラスを作成する上で最低限必要な作業は以下の2つです。

②:カスタム要素の文書構造を適用させるためのシャドウルートの作成
③:作成したシャドウルートに対してカスタム要素の文書構造の定義

最後に④として、作成したMyCounterクラスを<my-counter>というタグとして登録することで、カスタム要素の作成および登録が完了です。

それでは作成した<my-counter>タグをHTMLで呼び出してみましょう。

【index.html】

 <!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Components サンプル</title>
</head>
<body>
  <my-counter></my-counter>


  <script src="./define.js"></script>
</body>
</html>


先程のdefine.jsはbodyの最後で読み込むようにします。

<my-counter>タグは他のHTML要素と同じように記述することができます。

これで「Count Up」ボタンをクリックしてもカウントは変更されませんが、UIとしてのあつまりを一つのカスタム要素として定義することができました。

次の章では、「カスタム属性」「ライフサイクルイベント」を駆使して実際にカウントが加算されるようにするための処理について解説します。


Web Componentsでカスタム要素を作ってみよう(応用編)

以下の完成コードをもとに、基本編で作成したカスタム要素に対して、「Count Up」ボタンをクリックしたらカウントが加算されるようにする方法について解説いたします。

【define.js】

 class MyCounter extends HTMLElement {

  // ⑤
  static get observedAttributes() {
    return ['count'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    // ①
    const count = this.getAttribute("count") || 1;
    this.shadowRoot.innerHTML = `
      <style>
        #counter {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          width: fit-content;
          padding: 20px;
          border: 1px solid #ccc;
          border-radius: 0.5rem;
          background-color: #fff;
        }
        #count {
          font-size: 3rem;
          font-weight: bold;
        }
        #countup {
          margin-top: 1rem;
          padding: 0.5rem 1rem;
          border: 1px solid #ccc;
          border-radius: 0.5rem;
          background-color: #fff;
          font-size: 1rem;
          font-weight: bold;
          cursor: pointer;
        }
      </style>


      // ②
      <div id="counter">
        <span id="count">${count}</span>
        <button id="countup">Count Up</button>
      </div>
    `;


    // ③
    const countup = this.shadowRoot.querySelector("#countup");
    countup.addEventListener("click", this.countUp.bind(this));
  }


  // ④
  countUp() {
    const count = this.getAttribute("count") || 1;
    this.setAttribute("count", parseInt(count, 10) + 1);
  }


  // ⑥
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "count") {
      this.shadowRoot.querySelector("#count").textContent = parseInt(newValue, 10);
    }
  }
}

customElements.define("my-counter", MyCounter);


カスタム属性

①では、<my-counter>タグに指定できるcountというカスタム属性を受け取る処理を行っています。

②では、実際に受け取ったカスタム属性をUIの一部として反映しています。

この例では、以下のコード例のようにカウントの初期値を指定して呼び出すことができるようになります。

 <!-- カスタム属性を指定していないため0から始まる -->
<my-counter></my-counter>


<!-- count属性を指定しているため5から始まる -->
<my-counter count="5"></my-counter>


このように自由にカスタム属性を受け取り利用することができるため、呼び出し方によってはstyleを変更したり、表示される内容を変更したりといったカスタマイズ性を設けることが可能となっています。

イベントの発火

カスタム要素では通常のタグと同様にイベントを設定することが可能です。

③では「Count Up」ボタンに対してクリックイベントを設定しています。

④ではクリックイベントが発火した際に呼び出される関数を定義しており、このタイミングでカスタム属性であるcount属性の値をインクリメントして再設定するといった処理を行っています。

しかし、このままでは更新されたcounter属性の値がUIに反映されることはありません。

更新されたカスタム属性の値をUIに反映するには、次のライフサイクルイベント内で処理する必要があります。

呼び出されるタイミング

Web Componentsには4種類のライフサイクルイベントが存在します。

connectedCallback文書に接続された要素にカスタム要素が追加されるたびに呼び出されます。これはそのノードが移動するために発生するので、要素の内容が完全に解釈される前に発生することがあります。
disconnectedCallbackカスタム要素が文書の DOM から切断されるたびに呼び出されます。
adoptedCallbackカスタム要素が新しい文書に移動するたびに呼び出されます。
attributeChangedCallbackカスタム要素の属性が追加、削除、変更されるたびに呼び出されます。どの属性の変更を検知するかは static get observedAttributes() メソッドで指定します。

今回はカスタム要素の属性が変更されたことを検知したいため、attributeChangedCallbackを利用していきます。

そのために、まずは⑤で検知したいcount属性をstatic get observedAttributes()メソッドで指定しています。

そして、⑥のattributeChangedCallbackメソッド内で更新された属性がcountであれば、UIに新しく設定された値を反映する処理を行っています。

以上で、「Count Up」ボタンをクリックしたらカウントが加算されるようにする実装が完了となります。


Web Componentsを簡単に扱える『Lit』とは

ここまでWeb Componentsに関する説明を行ってきたのですが、Web Componentsをもっと扱いやすくするためのライブラリも存在いたします。

2021年4月22日にリリースされた「Lit」についても簡単にご紹介いたします。

Litは、かつてGoogleのChrome開発チームから発足したPolymer Projectによって開発されていたPolymer(Web ComponentsのPolyfill実装)が、Polymer → LitElement → Litと変遷を辿ったものとなっています。

主な特徴は以下です。

  • シンプルに扱える
    WebComponentsを扱う上で煩雑になりがちな定型文的な定義を減らすことで、シンプルな定義で生産性を高めて開発を行えるようになっています。
  • 動作が高速である
    Lit は約 5 KB (縮小および圧縮) となっているため、バンドルの読み込み時間が短いです。また、Lit は更新時にUIの動的部分のみを操作するため、レンダリング面でも非常に高速です。仮想ツリーを再構築してDOMとの差分を計算する処理は必要ありません。


『Lit』でカスタム要素を作ってみよう

Web Componentsで作成した<my-counter>タグを、Litで書き換えてみましょう。

【define.js】

 import {LitElement, html, css} from 'https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js';


export class MyCounter extends LitElement {
  static properties = {
    count: {type: Number},
  };


  static styles = css`
      #counter {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: fit-content;
        padding: 20px;
        border: 1px solid #ccc;
        border-radius: 0.5rem;
        background-color: #fff;
      }
      #count {
        font-size: 3rem;
        font-weight: bold;
      }
      #countup {
        margin-top: 1rem;
        padding: 0.5rem 1rem;
        border: 1px solid #ccc;
        border-radius: 0.5rem;
        background-color: #fff;
        font-size: 1rem;
        font-weight: bold;
        cursor: pointer;
      }
  `;


  constructor() {
    super();
    this.count = 1;
  }


  render() {
    return html`
      <div id="counter">
        <span id="count">${this.count}</span>
        <button id="countup" @click=${this.countup}>Count Up</button>
      </div>
    `;
  }


  countup() {
    this.count++;
  }
}


customElements.define('my-counter', MyCounter);


【index.html】

 <!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Components サンプル</title>
  <script type="module" src="./define.js"></script>
</head>
<body>
  <my-counter></my-counter>
</body>
</html>


LitではNPMを使ってインストールする方法もございますが、特別な環境構築も不要で直接CDNからimportして使い始めることもできます。

上記のコードのように、Web Componentsでは煩雑であった以下のような箇所を、簡単に理解しやすい形で実装することができます。

・カスタム属性や公開しないプロパティなどの扱い方を詳細に設定することができる
・イベントハンドリングを「@click」などの特別な属性で定義することができる
・HTMLとスタイルを分けて定義することができる

ここでは触れられていない便利な機能もまだございますので、ぜひ公式ドキュメントをご参照ください。

また、Webサイト上でLitを試すことができる「Lit Playground」も提供されていますので、ぜひ活用してみてください。


Web Componentsは多くの利点がある手法

Web Componentsにおける最大のメリットは「相互運用性」と「将来互換性」だと考えています。

ReactやVue、将来生まれてくるフレームワークに対しても、WebComponentsは組み合わせて利用することが可能となっています。

うまく活用して開発効率・保守性の高い開発をしていきたいですね!

株式会社GIGは、ナショナルクライアントからスタートアップまで、Webコンサルティング、UI/UXデザイン、システム開発など、DX支援をおこなうデジタルコンサルティング企業です。また、45,000人以上が登録するフリーランス・副業向けマッチングサービス『Workship』や、7,000人以上が登録するデザイナー特化エージェントサービス『クロスデザイナー』、リード獲得に特化したCMS『LeadGrid』、UXコンサルティングサービス『UX Design Lab』などを展開しています。

DX支援のご相談はいつでもご連絡ください。

■株式会社GIG
お仕事のお問い合わせはこちら
会社紹介資料のダウンロードはこちら
採用応募はこちら(GIG採用サイト)
採用応募はこちら(Wantedly)

WebやDXの課題、お気軽にご相談ください。

石倉 彰悟(いっしー)

株式会社GIG SkillShare事業本部 開発事業部 事業部長 ソーシャルゲーム会社でカスタマーサポートとして従事した後にエンジニアに転身し、大規模決済システムやEC系Webサービス等の構築を経験。2018年にGIGにジョインし「Workship」の開発組織の責任者としてより良いサービス提供を行うための施策立案、開発を行っている。