Panda Noir

JavaScript の限界を究めるブログでした。最近は幅広めに書いてます。

祝WebComponents実装!実際にコンポーネントを作ってみた

いよいよWebComponentsが主要ブラウザでサポートされたそうなので、ライブラリ抜きで実装してみました。

WebComponentsを構成する要素

WebComponentsを実現するために次の4つの仕様が策定されました。

  • Custom Elements
  • HTML Templates
  • Shadow DOM
  • HTML Import

Custom Elements

WebComponentsはカスタム要素という形でHTMLに組み込まれます。というより、WebComponentsは安全にカスタム要素を作るための仕様と表現してもいいかもしれません。どうして安全なのかは次以降で説明していきますね。

Custom Elementsはカスタム要素を使えるようにする部分のみです。カスタム要素本体のHTMLなどはHTML Templatesなどが負担します。

HTML Templates

HTML Templatesはカスタム要素の雛形HTMLを作るために使います。<template>というHTML要素で作ります。

このtemplate要素自体はブラウザに表示されません。これを実体化(インスタンス化)することで初めて意味を持ちます。オブジェクト指向のクラスのようなイメージですね。

カスタム要素は複数作られることを想定しているので、このような雛形があるというのは自然な発想ですよね。

Shadow DOM

個人的にShadow DOMが一番重要だと思います。Shadow DOMは次のようなことができます。

  • 外部のCSSが内側の要素に影響しない
  • 内部のCSSも外部に影響しない
  • 中のHTML要素へのアクセスを禁止できる

3つ目はそこまで大事ではありませんが、前半2つが革命です。CSSがスコープを持てるからです。まあ、スコープなら外側のスコープを参照できるので微妙に違いますが。

内部のCSSが外部に漏れることもなく、外部のCSSのことも気にしなくていいので、class属性の衝突を気にする必要がなくなります。素晴らしいですね。

HTML Import

HTML Importは外部のHTMLファイルを読み込んでパースするAPIです。HTML Templatesを外部ファイルに置いて、それをHTML Importでインポートする、というふうに使います。

コード

以上を踏まえて実装するとこんなふうになります。

template.html

<!-- HTML Template -->
<template id="template">
  <style>
    a {
      color: red;
    }
  </style>
  <a href="http://example.com">in component</a>
</template>

component.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <style>
      /* 外部のCSSが内部に影響しないという検証用 */
      a {
        background: #ccc;
      }
    </style>
    <link rel="import" href="template.html"> <!-- HTML Import -->
  </head>
  <body>
    <my-component></my-component>
    <a href="http://example.com">not in component</a>
    <script>
      class MyComponent extends HTMLElement {
          constructor() {
              super();
              const shadow = this.attachShadow({mode: 'open'}); // Shadow DOMをつくる
              const imported = document.querySelector('link[rel="import""]').import; // HTML Importした要素をここで受け取る
              const template = imported.querySelector('#template');
              const instance = document.importNode(template.content, true); // template要素を実体化する
              shadow.appendChild(instance);
          }
      }
      customElements.define('my-component', MyComponent); // Custom Elements でカスタム要素として登録する
    </script>
  </body>
</html>

ちなみに

実はHTML TemplatesとHTML Importなしでも実装できます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <style>
      /* 外部のCSSが内部に影響しないという検証用 */
      a {
        background: #ccc;
      }
    </style>
    <link rel="import" href="template.html"> <!-- HTML Import -->
  </head>
  <body>
    <my-component></my-component>
    <a href="http://example.com">not in component</a>
    <script>
      class MyComponent extends HTMLElement {
          constructor() {
              super();
              const shadow = this.attachShadow({mode: 'open'}); // Shadow DOMをつくる
              // Shadow DOMに直埋めする!
              shadow.innerHTML = `
              <style>
                a {
                  color: red;
                }
              </style>
              <a href="http://example.com">in component</a>`;
          }
      }
      customElements.define('my-component', MyComponent); // Custom Elements でカスタム要素として登録する
    </script>
  </body>
</html>

ただし、この方法ではJavaScriptの中にHTMLを埋め込むという気持ちわるさが残ってしまいます。React先輩のディスはやめろ。そのため、HTML TemplatesとHTML Importできちんと分離しましょう。

終わりに

構成要素を分解して見てみるとWebComponentはかなり簡単です。今まで気後れしてた方もこれを機に書いてみてはいかがでしょうか?

今年最後にふさわしい記事を書けたので満足です。