ShadowDOM
WebComponents
Polymer

この記事は 一休.com Advent Calendar 2017 の 12 日目です。

CTO 室で 一休.com レストラン の開発支援をしています。Web フロントエンドエンジニアの稲尾です。

幸運にも 10 月末に San Francisco で開催された Chrome Dev Summit 2017 に行かせてもらいました。そこで Google の推進する Web Components におおいに感化されて来ました。Web フロントエンドのコンポーネント指向開発フレームワークとして標準化の進む Web Components を、Polymer を交えて、改めて紹介します。

tl;dr

  • コンポーネント指向は web フロントエンド開発の新標準
  • ページをつくるのではなく、コンポーネントをつくって組み合わせる
  • Polymer は現実的なコンポーネント指向のフレームワーク
  • Web Components はコンポーネント指向の Rosetta Stone

コンポーネント指向とは

Web Components の話の前に、まず「コンポーネント指向」のイメージを持つ事が大事です。

コンポーネント指向というコトバに明確な定義は存在しないので、色々な粒度で用いられる表現だとは思いますが、ここでは次のように表現してみます:

  • ユーザー視点
    • 統一感のあるトンマナによる一貫性のあるユーザー体験
  • デベロッパー視点
    • 再利用性のある設計

前者は、web をもっとアプリのようにするものだと思います。アプリは、悪く言えば限られた表現の中でユーザー体験を提供します。OS のガイドラインに沿った画面レイアウト、SDK が提供するカスタマイズ範囲の限定された UI 部品、限定された動作環境... もちろんゲームアプリのようにゼロから UI を設計する事も可能ですが、アプリの多くはそういった事をしません。それは「限られた表現」であるからこそ、一貫性を提供できるからだと思います。

iPhone アプリであれば、まったく別のアプリであっても、ある程度は迷わずすぐ使いこなせるのは、OS のデザインガイドラインがあって、SDK が提供する UI 部品に共通性があるからです。

ユーザー視点でコンポーネント指向を考える時に一番重要なのは、ユーザー体験の一貫性だと思います。

つづいて後者は、前者を提供するメカニズムだと思います。「限られた表現」とは OS や SDK が開発者に課す制約ですが、逆に言えば、一貫性を提供するためのメカニズムです。再利用性のある設計をする事で、部品の再利用が文字通り発生し、結果として一貫性をつくり出します。

この 2 つが「コンポーネント指向」のキモだと思います。

Web Components とは

Web Components はコンポーネント指向のための標準化されたメカニズムです。

以下に挙げる 4 つの標準技術で構成されています:

Template Element

Template Element とは、 平たく言えば <script type="text/template"></script> の標準化です。HTML の中に JavaScript テンプレートを書く時に用いられてきたこのコードを標準仕様として策定したのが <template></template> です。

<template> タグ内に書かれた HTML コードは、一切評価されません。

<template id="my-template">
  <style>p { font-size: 100pt; font-weight: bolder; color; red; }</style>
  <script>alert('Hello!');</script>
  <p>Hello!</p>
</template>

<template> タグの評価には明示的な処理が必要です。

const myTemplate = document.querySelector('#my-template');
const clone = document.importNode(myTemplate.content, true);
document.body.appendChild(clone);

Custom Elements

Custom Elements は HTML 要素を自由に拡張するための仕様です。

例えば <my-element> という要素を新たにつくりたい時は;

<my-element>Hello!</my-element>

上記のような HTML タグを書き;

class MyElement extends HTMLElement {
  constructor() {
    super();
    // attachShadow/<slot> は後述する Shadow DOM の機能
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <p>
        <slot></slot>
      </p>
    `;
  }

  connectedCallback() {
    /* 要素が DOM に挿入された時のコールバック */
  }

  disconnectedCallback() {
    /* 要素が DOM から削除された時のコールバック */
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    /* 属性が更新された時のコールバック */
  }

  adoptedCallback() {
    /* 新しい document に移動された時のコールバック */
  }
}

customElements.define('my-element', MyElement);

上記のような JavaScript で振る舞いを定義して customeElements.define でカスタム要素として登録してやる事で、標準の HTML タグと同様に扱う事ができるようになります。

my-element {
  font-size: 100pt;
  font-weight: bold;
  color: red;
}

my-element:not(:defined) {
  /* customeElements.define() で登録されているかどうかは :defined セレクタで判定可能 */  
}

もちろん CSS も通常の HTML 要素と同様にスタイルを定義する事ができます。

この他にも;

  • 既存の HTML 要素をオーバーライドしたり
  • JavaScript の振る舞い追加を遅延処理したり
  • <template> と連携したり
  • 後述する Shadow DOM と連携したり

できます。

Shadow DOM

Shadow DOM は <iframe> のようにページ内にカプセル化された部品を挿入できる仕組みです。 <iframe> との違いは、実際には同じページのコンテキストを共有できること、そして、その上でカプセル化が維持されることです。

カプセル化の示す機能は次の 2 つです:

  • Isolated DOM
    • Shadow DOM は自己完結であり親 DOM の document.querySelector() などの影響を受けません。
  • Scoped CSS
    • Shadow DOM 内部の CSS は親 DOM のスタイル定義の影響を受けません。

これにより、ひとつのページ内部に、自己完結した部品をつくる事が可能になります。

Shadow DOM の作成は次のようなコードを実行します:

const section = document.createElement('section');
const shadowRoot = section.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<p>Hello.</p>';

上記のように attachShadow() によって生成される shadowRoot が、上記のカプセル化の特性を持った DOM (#shadow-root) となります。

<slot>

Shadow DOM の重要な機能の 1 つが <slot> です。これは #shadow-root 内部をカスタマイズするためのプレースホルダーです。ユーザーはここに独自のマークアップを入れることができます。

<!-- 前項の Custom Element 例 -->
<my-element>Hello!</my-element>

<!-- #shadow-root を展開した疑似コード -->
<my-element>
  #shadow-root
    <p>
      Hello!
    </p>
</my-element>

:host

:host は Shadow DOM の仕様に定義された CSS セレクタで、 <my-element> の例で言うと <p> 要素を指します。#shadow-root 内部の親要素を指すセレクタです。

JavaScript modules

JavaScript modules は ES Modules として標準化されている JavaScript のモジュール化を HTML からも利用できるようにするための仕様です。

<script type="module" src="module.js"></script>

JavaScript 内での importexport に加えて、上記のような type="module" 属性によるモジュールスクリプトの宣言が可能になります。

HTML Imports

HTML Imports は Web Components 標準化の初期に Google が提唱した HTML のモジュール化の仕様です。

<link rel="import" href="/path/to/module.html">

上記のようなタグを書くことで、 CSS の @import のように HTML 部品をインポートする事ができます。

この仕様は各社 UA ベンダーに受け入れられる事なく、特に Mozilla の ES Modules の動向に期待するという表明もあって (?) 事実上 Chrome だけが実装した機能となっています。こうした流れから、モジュール化は HTML Imports から、前述の JavaScript modules に移行していく見通しです。

Web Components の web ブラウザ対応状況

さて、ここまでに挙げた標準仕様の各 web ブラウザの対応状況は次の通りです:

IE Edge Firefox Chrome Safari iOS Safari Chrome for Android
Template Element ×
Custom Elements v1 × × ×
Shadow DOM v1 × × ×
JavaScript modules × ×
HTML Imports × × × × ×

現状だけ見ると、 Microsoft の Edge と Mozilla の Firefox は中核の仕様に未対応です。しかし Edge は Considering、Firefox は Developing のステータスにあり、主要 web ブラウザが Web Components 仕様を実装するのは時間の問題です。

また、未対応の仕様についても、次に挙げる Polymer を使用する事で polyfill が提供され、未対応 web ブラウザでも Web Components 仕様に沿った web 開発が可能です。

Polymer とは

Polymer とは Google が開発する JavaScript ライブラリで、 Material Design に端を発したコンポーネント指向を実現するためのフレームワークです。

現在の安定板 Polymer 2.0 は、前述の HTML Imports をモジュール化の軸とした Web Components で構成されており、コンポーネントの定義には次のような HTML を書きます。

<!-- HTML Imports -->
<link rel="import" href="../bower_components/polymer/polymer-element.html">

<dom-module id="my-element">

  <!-- Template Element -->
  <template>
    <style>
      /* #shadow-root へのスタイル定義 */
      :host {
        font-size: 100pt;
        font-weight: bold;
        color: red;
      }
    </style>

    <p>
      <!-- Shadow DOM のプレースホルダー -->
      <slot></slot>
    </p>
  </template>

  <script>
    // Polyer.Element は HTMLElement のサブクラス
    class MyElement extends Polymer.Element {
      static get is() { return 'my-element'; }
    }

    // Custom Element として <my-element> を登録
    customElements.define(MyElement.is, MyElement);
  </script>

</dom-module>

見てわかる通り Web Components で標準化された技術の組み合わせて記述されています。 Polymer.Element クラスも HTMLElement クラスを継承した小規模なラッパークラスです。

Polymer はコンポーネント指向を実現するためのメカニズムを提供する事を目的としており、Angular のような大規模な JavaScript フレームワークと比較すると、かなり小規模です。

機能の大半を Web Components という標準技術に委任しており、 graceful degradation のための polyfill を提供すること、データバインディングのための最小限の機能を提供することに絞っており、開発者視点では「ライブラリ」と呼ぶのが正しいかも知れません。

Web Components の polyfill としての Polymer

Custom Elements と Shadow DOM は Edge と Firefox が現状、未対応ですが、Polymer を通して Web Components を使用すると、対応 web ブラウザは次のようになります:

IE Edge Firefox Chrome Safari iOS Safari Chrome for Android
Template Element
Custom Elements
Shadow DOM
JavaScript modules
HTML Imports

Polymer に含まれる webcomponents-lite.js / webcomponents-loader.js が Custom Elements と Shadow DOM の polyfill として機能し、 custom-elements-es5-adapter.js により IE11 向けの ES5 環境への polyfill を提供します。

これによって、Polymer を使用すれば IE11 を含むモダン web ブラウザで Web Components の標準仕様に沿った開発が可能になります。

Polymer App Toolbox

Polymer の魅力はライブラリそのものと言うより、周辺技術によるモダン web 開発へのサポート体制と、コンポーネント指向のエコシステムです。

Polymer App Toolbox は、 Polymer プロジェクト作成のための boilerplate や CLI、PWA のための基礎的なコンポーネント群の総称です。

CLI のインストールは;

yarn global add polymer-cli

のような感じでできます。

Polymer コンポーネント

Polymer 向けのプリミティヴなコンポーネントは WebComponents.org という web コンポーネントを公開/ダウンロードできる web サイトで公開されており、 Bower を通してインストールできます。

App Shell モデル を実現するための;

フロントエンドルータのための;

フロントエンドの永続データ処理のための;

PWA の主要技術 Service Worker のための;

などの基礎的なコンポーネントを提供しています。

Polymer コマンドラインツール

CLI はプロジェクト boilerplate の生成だけでなく、ビルドプロセスにも対応しています。昨今のベストプラクティスを反映したツールセットとなっており polymer.json に必要な設定を書くだけで、柔軟なバンドル構成でビルドしてくれます。

また PRPL パターン でのバンドル構成のビルド支援も充実しており、 Polymer と組み合わせた PRPL パターンを実現する BFF の実装も提供されています。

Polymer 3.0

安定板の Polymer 2.0 は、コンポーネント管理に Bower を採用していたり、各社ベンダーから賛同か得られなかった HTML Imports をモジュール化に採用していたりと、残念な部分も見受けられます。

しかし Polymer 3.0 ではこれらが解消される予定となっており;

  • HTML Imports から ES Modules への移行
  • Bower から NPM への移行
  • lit-HTML による JSX-like な HTML in JS の実現

などが決定しています。

データ管理についても Redux の活用など実践的なノウハウも既に共有されており YouTube のような Google の主要なサービスで Polymer による実装が進んでいます。

Web Components as Rosetta Stone

Chrome Dev Summit 2017 2 日目の Frameworks Panel で、web フロントエンドフレームワークの開発者によるパネルディスカッションがありました。

その中で印象的だった言葉が "Rosetta Stone" です。

Frameworks Panel ハイライト

React, Preact, Polymer, Angular, RxJS, webpack, Vue.js などのコアデベロッパーが会したディスカッションで webpack/Vue.js のデベロッパ Sean Larkin が端を発した発言から Angular デベロッパの Rob Wormald が;

So I feel like the Rosetta Stone is that Sean was talking about. I think that's what a Custom Element is. [...]

I think it's very sad, as a web developer, that everybody has to rewrite the datepicker in every framework that exists, right?

で拍手喝采。

Web Components の標準化が "Datepicker の再発明の終焉" であり、再利用性のための web デベロッパのための共通言語 "Rosetta Stone" であるという事が、ディスカッション全体の空気になった瞬間でした。

このパネルディスカッションでおもしろかった点として、React の Andrew Clark が、この空気とは一定の距離を置いていた点でした。曰く;

Web Components の encapsulation の実現と comsumption の解消は cool だけど React の中核は memoization で、最新の React はコアの処理を全て async にしたのとこれらが Virtual DOM と密接に結合している (超訳)

との事で、 Virtual DOM をコアとする React が Custom Elements 互換の Web Components 標準に寄り沿うのは、少し難しそうな印象でした。

ただ WORA ("write once, run anywhere") モデルを掲げる web だからこそフレームワークごとに datepicker を再発明するのはもう止めようというスローガンのが、個人的に刺さりました。

Web Components 標準化の流れに待ったナシです :muscle:

明日は @kitsuyui の「データエンジニアとデータの民主化 〜脱・神 Excel 〜」です。