Uzabase Tech Blog

SPEEDA, NewsPicks, FORCASなどを開発するユーザベースの技術チームブログです。

WebComponentsを使ってみよう(その1)

こんにちは、SaaS Product Teamのとみたです。

今回は、WebComponentsについて調べてみたことを書きます。その1として、 カスタムエレメント に注目して書いています。ほかの項目については、また別の機会に書きます。

また、今回のサンプルコードと、デモです。

WebComponentsとは

カスタムエレメント、ShadowDOM、HTMLTemplateといった技術的要素をまとめた言葉です。 カスタムエレメントとは、divinputのようなHTMLElementと同様にmy-web-componentsのように独自のコンポーネントを定義し、使うことができる仕組みです。 コンポーネントの利用者は、自分のアプリケーションに<my-web-components>という感じでコンポーネントを差し込むだけで、inputなどのエレメントと同じように機能させることができるようになります。

ShadowDOMとは、外部からアクセスできないようにしたDOMツリーを構築することができるようになる要素のことです。ShadowDOMをルートエレメントとして、ぶら下げたDOMツリーは外側のCSSに影響されず、querySelectorなどで列挙することが難しくなります。これにより、スタイルやスクリプト化をより自由に行えるようになります。

HTMLTemplateとは、template, slotという特殊なタグのことを指します。DOMの構造を再利用しやすくする機能です。

マイクロフロントエンドのやりかたの一つとして挙げられることが多いようです。

iframeとの違い

マイクロフロントエンドとして、iframeでアプリケーションを埋め込む場合がありますね(embedやobjectと呼ばれるさらに古いものもあるようですが、よく知りません)。たとえば、このはてなブログもポップアップするエディター画面はiframeで埋め込まれています。WebComponentsもiframeもアプリケーションに何かを埋め込むという点では同じことができます。

大きな違いは何かというと、動作するホストでしょう。

iframeはアプリケーションとは別のURLのhtmlを埋め込むことができ、iframe内では親のホストとは異なるホストで動作します。iframe用のアプリケーションを提供する側はCORSを気にする必要がありません。例えば、Google Mapをiframeで埋め込んでいますが、どこに埋め込まれているかを気にせずに利用することができています。

一方で、WebComponentsは親のホスト上で動作することになるため、コンポーネントを提供する側はCORSを考慮したWebAPIを用意する必要があります。

他にも、カスタムエレメントは他のHTMLElementと同様に周りのエレメントの幅や高さに影響されますが、iframeは幅や高さが影響されず、iframe内でスクロールします。これは利点でもありますが、スクロールさせたくない場合には欠点にもなるでしょう。

俺のカスタムエレメントを作ってみる

1. 時計

まずは、時計を作成しましょう。

これで最低限、カスタムエレメントをどのように定義し、描画するかを学びます。

class SimpleClock extends HTMLElement {
  constructor() {
    super();
    // 1. HTMLから分離されたノード(ShadowDOM)を作成する。
    //    これで、親のドキュメントのCSSなどに影響されない環境を作成できます。
    this.shadow = this.attachShadow({mode: 'open'}); 
    // 2. 今の時間を表示する h1 エレメントを ShadowDOM 下に作成する。
    const now = new Date();
    const clockElement = document.createElement("h1");
    clockElement.textContent = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`;
    this.shadow.append(clockElement);
  }
}

(function main() {
  // 3. simple-clock という名前でSimpleClockクラスを登録する
  customElements.define("simple-clock", SimpleClock);
})();

このJSをhtmlから呼び出して、<simple-clock>というカスタムエレメントを配置すれば、ページをロードした時間が表示されるようになるでしょう。

<!-- simple-clock.jsという名前でアクセスできるものとする -->
<script src="simple-clock.js"></script>
<simple-clock></simple-clock>
※注意点
  • カスタムエレメントの名前はdivinputなどの通常の要素と区別するため、ハイフンで区切られた名前である必要があります。

2. チャート

続いて、グラフを表示しましょう。チャートは外からデータを受け取り、そのデータをグラフに描画します。チャートライブラリとしてC3.jsを利用します。

ここでは、外からデータを受け取る方法と、独自のスタイルシートを適用する方法を学びます。

import * as c3 from "c3";

class SimpleChart extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    // 1. ShadowRootのinnerHTMLに link タグを入れることで、ShadowRoot内限定で外部のCSSをロードできます。
    //    style タグも利用できます。
    this.shadowRoot.innerHTML = `
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/c3.css">
      <div></div>
    `;
    // 2. ここは重要ではありませんが、c3.jsはShadowRootに直接バインドできなかったので div エレメントを用意しています。
    this.div = this.shadowRoot.querySelector("div");
    // 3. attribute "columns" に文字列でデータが渡されることを想定しています。
    //    '[["data1", 3, 2, 1], ["data2", 1, 2, 3], ...]' のような c3.js の columns プロパティ用のデータです。
    const columns = JSON.parse(this.getAttribute("columns"));
    this.renderedChart = c3.generate({
      bindto: this.div,
      data: {
        columns,
      },
    });  
  }
}

これは以下のように利用できます。

<simple-chart columns='[["data1", 1, 2, 3], ["data2", 10, 9, 8]]'></simple-chart>
※注意点
  • 外からなんらかのデータを渡すには、jsxのpropsなどと同様にAttributesを利用します。なお、通常のHTMLタグと同様文字列のみが利用できます。AttributeでなくWebComponentsのエレメントのプロパティにデータをセットすることもあるかもしれませんが、あまり良い方法ではないでしょう。
  • CSSを読み込む方法はこちらを参考にするとよりよいでしょう。

3. Attributesが更新されたら再描画する

class SimpleFormatter extends HTMLElement {
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    // 1. 描画には render(再描画でも使う) を使うようにします
    this.render();
  }

  static get observedAttributes() {
    // 2. 変更を監視するAttributeの名前を配列で複数指定できます。
    //    ここに書かれたAttributeが変更された場合、attributeChangedCallbackが呼び出されます。
    return ["name"];
  }

  attributeChangedCallback() {
    // 3. Attributeが更新されたら、attributeChangedCallbackが呼ばれます。
    //    isConnectedがtrueであれば、すでに描画済みです。
    //    attributeChangedCallbackは初回の描画よりも先に呼ばれることがあり、
    //    その際にrenderするのを避けるためにチェックしています。
    if (this.isConnected) {
      this.render();
    }
  }

  render() {
    const name = this.dataset.name;
    this.shadow.textContent = name;
  }
} 

Reactなどからこのように呼び出すことができます。

const App = () => {
  const name = ...
  return <simple-name name={name} />;
}

まとめ

WebComponentsの基本的な作り方を書きました。

  • ShadowRootを使えばスタイルが外部に影響されないものを作ることができる(外からスタイルを適用する方法もある)
    • 今回はShadowRootしか使っていませんが、代わりに普通のdivエレメントなどを使うことも可能です。
    • また、ShadowRootはWebComponents専用の機能ではなく、普通に利用できます。
  • カスタムエレメントのタグ名には - ハイフンが必須
  • CSSは link タグや style で適用できる

サンプルコードの話

  • components/clock: 時計のカスタムエレメントです。
    • Reactで書かれていて、250ms間隔で時間を更新するようになっています。
  • components/chart: チャートのカスタムエレメントです。
    • 素のjsとc3jsで書かれています。
    • チャートの上にinput要素を表にして並べています。ここに数値を入れたり、+で要素を増やしたりすると、チャートが更新されるようになっています。attributeChangedCallbackで更新されます。
    • c3.js用のCSSをCDNから取得しています。adoptedStyleSheetsの機能を使いたかったのですが、なぜか利用できませんでした。まだ対応してないのでしょうか。
  • app: 上記2つのカスタムエレメントを利用しているアプリケーションです。
    • Reactで書かれています。