HTML5Experts.jp

Web ComponentsのこれからーPolymer 0.8、X-Tag、Brick、Bosonic

この記事は、連載「基礎からわかるWeb Components徹底解説~仕様から実装まで理解する〜」の第5回目になります。今回は、先日発表されたPolymer 0.8の変更点、MozillaのWeb Componentsへの関わり方やX-Tag、Brick、BosonicといったPolymer以外の周辺ライブラリについて紹介します。

Polymer 0.8のリリース

先日、Polymerのバージョン0.8がリリースされました。

0.8は1.0のリリースに向けたアルファリリースと位置付けられています。APIも正式版への提案として大きく変更が加えられており、これまでとの互換性も保たれていません。既に実践でPolymerを使っていて、0.8へアップデートを行う場合はこれまでのコンポーネントを作り直す必要があります。

アルファ版ということで、ユーザーのフィードバックや再設計を経て、さらなる変更がある可能性もありますが、より完成度の高いライブラリに近づいていくことでしょう。アップデートに伴い、どのような変更があるのかを見ていきます。

ポリフィルライブラリ

これまでポリフィルライブラリとして推奨されてきたwebcomponents.jsですが、以降はwebcomponents-lite.jsが推奨されています。webcomponents.jswebcomponents-lite.jsの差はShadow DOMのポリフィルを含んでいるかどうかなのですが、Polymer 0.8ではよりシンプルで軽量な shady DOM というShadow DOMのポリフィルを含んでいるためです。

異なる用途でShadow DOMのポリフィルを必要とする場合は別ですが、Polymerを利用するためであれば、webcomponents.jsの重いShadow DOMのポリフィルを含まず、軽量なwebcomponents-lite.jsを選択するのが望ましいと言えます。

軽量化とパフォーマンスの向上

APIの大きな見直し(後述)と抜本的なリファクタリングによって、ライブラリのファイルサイズの軽量化と実行パフォーマンスの向上が図られています。以下はPolymerLabs/benchmarksで試すことができるベンチマークの結果です(Polymer公式サイトより引用)。初期描画までの時間の棒グラフになっており、低ければ低いほど描画までの時間が短くパフォーマンスが良いということになります。

いくつかのデバイス・ブラウザで計測されていますが、いずれも0.5に比べて0.8が数倍高速に動作するという結果が出ています。もとよりWeb ComponentsはHTML ImportsのCustom Elementsの遅延評価されるという性質によるコンポーネントのがたつきやチラつきが問題になりがちですが、最も重要である初期描画までのパフォーマンスが向上されているのは嬉しいところです。

互換性のない大きなAPIの変更

冒頭で述べた通り、0.5以前と互換性はありません。

リリースノートとマイグレーションガイドを元に主な変更点を見ていきます。さらなる詳細な差分については、公式ドキュメントを確認してください。

カスタム要素の登録

これまでは、<polymer-element>要素を用いてカスタム要素の登録を行ってきました。

<polymer-element name="my-element">
  <template>
    <style>
      div { color: red; }
    </style>
    <div>Shadow DOM in My Element</div>
  </template>
  <script>
    Polymer();
  </script>
</polymer-element>

0.8からは以下のように、<dom-module>を起点にした方法に変更されます。<dom-module>要素のIDに登録したいカスタム要素名(ここではmy-element)を指定し、PolymerコンストラクタでそのIDを参照しています。

<dom-module id="my-element">
  <style>
    div { color: red; }
  </style>
  <template>
    <div>Shadow DOM in My Element</div>
  </template>
</dom-module>

<script> var MyElement = Polymer({is: 'my-element'}); </script>

Polymer({is: 'my-element'});の実行より前に、<dom-module>がロードされている必要があります。<style>タグを<template>の外に配置するようになった点にも注意して下さい。要素の継承についても同様にextendsキーワードで行うことができますが、0.5以前では可能であったカスタム要素の継承は廃止され、<button>や<textarea>のようなビルドインのHTML要素のみになっています。また、これまではconstructor属性にコンストラクタ関数名を指定していましたが、0.8では属性が廃止されるとともにPolymer()がカスタム要素のコンストラクタを返却するようになりました。

document.registerElementの第二引数に指定するextendsの値にもカスタム要素名を指定することはできず、コンストラクタ関数の返却もdocument.registerElementが行います。いずれも素の挙動に近い振る舞いになったと言えるでしょう。

カスタム要素のパースの順序

カスタム要素を含むカスタム要素を作成する場合は、内包するカスタム要素が事前に登録されている必要があります。つまり、<child-element>を含む<parent-element>を作成する場合、<child-element>が事前に登録されていなければなりません。

デフォルト属性とプロパティの宣言

カスタム要素のデフォルト属性とプロパティ宣言の方法も変わります。0.5までは以下のように、デフォルト属性は<polymer-element>に、プロパティの宣言はattributesにスペース区切りで指定していました。

<polymer-element name="your-element" attributes="foo bar" role="button" layout horizontal wrap>
  <template>
    <button>Your Element</button>
  </template>
  <script>
    Polymer({
      foo: 0,
      bar: 'Hello!'
    });
  </script>
</polymer-element>

0.8以降は以下のように、Polymer()コンストラクタにhostAttributespropertiesを渡すことで宣言します。

<dom-module id="your-element">
  <template>
    <button>Your Element</button>
  </template>
</dom-module>

<script> Polymer({ is: 'your-element', hostAttributes: { role: 'button', class: 'layout horizontal wrap' }, properties: { foo: { type: Number, value: 0 }, bar: { type: String, value: 'Hello!' } } }); </script>

layouthorizontalwrapといったようなレイアウトに関するカスタム属性も通常のCSSクラスになり、hostAttributesclassに指定します。また、このレイアウト機能はPolymerのコアから分離されpolymer.htmlに同梱されなくなったので、別途layout.htmlをインポートする必要があります。

オブザーバーの登録

プロパティの値の監視や、宣言的なイベントハンドラ登録といった機能もPolymerの強力な機能のひとつですが、これらにも少々変更があります。

これまでは、fooというプロパティに対してfooChangedというオブザーバー関数を宣言すると命名規則によって自動でバインドされる機能がありました。また、IDを振った要素はthis.$.idという形でコンストラクタ内でノードを参照することが可能であり、それをobserversに定義することでもプロパティの監視が可能でした。

<polymer-element name="your-element" attributes="foo">
  <template>
    <input type="text" id="input">
  </template>
  <script>
    Polymer({
      foo: 0,
      observers: {
        'this.&.input.value': 'inputValueChanged'
      },
      fooChanged: function (oldValue, newValue) {
        console.log('newValue is ', newValue);
      },
      inputValueChanged: function () {
        console.log('this.&.input.value is changed');
      }
    });
  </script>
</polymer-element>

0.8からはこのpropertyNameに対してpropertyNameChangedという関数を定義することで自動バインドされる仕組みと、IDを振った要素がthis.$にぶら下がる機能が廃止されます。これによってプロパティの監視を行うには、以下のように、ブラケットで囲った変数に対し、observer属性にオブザーバー関数を明示的に宣言することになります。

<dom-module id="your-element">
  <template>
    <input type="text" value="{{inputValue}}">
  </template>
</dom-module>

<script> Polymer({ is: 'your-element', properties: { foo: { type: Number, value: 0, observer: 'fooChanged' }, inputValue: { observer: 'inputValueChanged' } }, fooChanged: function (oldValue, newValue) { console.log('newValue is ', newValue); }, inputValueChanged: function () { console.log('inputValue is changed'); } }); </script>

暗黙的にオブザーバー関数がバインドされる分、直感的に解り難い機能であったので、このように明示的な宣言方法になるのは嬉しいところです。

イベントハンドラの登録とデータバインディング

ブラケット{{}}によるデータバインディングや、イベントハンドラの登録もPolymerお馴染みの機能ですが、これらにも少々変更があります。

<polymer-element name="our-element">
  <template>
    <input type="text" value="{{firstName}}">
    <input type="text" value="{{lastName}}">
    <button on-click="{{onClick}}">Say full name</button>
  </template>
  <script>
    Polymer({
      onClick: function () {
        alert("{{firstName + ' ' + lastName}}");
      }
    });
  </script>
</polymer-element>

0.8からは{{}}内の式の評価はサポートされなくなります。

この例で言えば、"{{firstName + ' ' + lastName}}"のような表現はできなくなります。代わりにcomputedを用いて、式評価後の値を参照するプロパティを宣言する必要があります。これは0.5以前とは異なり、0.8からはpropertiesの配下に定義する属性になることに注意してください。

同じく、{{}}を使ってon-click="{{onClick}}"のように定義していたイベントハンドラの登録は、カーリーブラケットを使わずに記述します。

<dom-module id="our-element">
  <template>
    <input type="text" value="{{firstName}}">
    <input type="text" value="{{lastName}}">
    <button on-click="onClick">Say full name</button>
  </template>
</dom-module>

<script> Polymer({ is: 'our-element', properties: { firstName: String, lastName: String, fullName: { type: String, computed: 'computeFullName(firstName, lastName)' } }, computeFullName: function (firstName, lastName) { return firstName + ' ' + lastName; }, onClick: function () { alert('{{fullName}}'); } }); </script>

このようにfullNamecomputed: computeFullName(firstName, lastName)とすることでcomputeFullNameの実行結果を参照することが可能です。

DOMの操作

Polymer内でDOM操作を行う場合、通常のShadow Rootから実行するDOMのAPIではなく、Polymer.domという専用のAPIを使う必要があります。

Polymer.domから行う追加や削除といったDOM操作は、パフォーマンスを考慮し遅延して実行するようになっています。そのため、操作後のノードの座標やgetComputedStyle()を使ったスタイルの取得をする場合は、操作をPolymer.dom.flush()を使って適時実行し、反映します。

// 従来のDOM操作
this.appendChild(node);
this.shadowRoot.appendChild(node);

// Polymer.domを使ったDOM操作 Polymer.dom(this).appendChild(node); Polymer.dom(this.root).appendChild(node);

いずれもDOMのAPIと同じ命名と引数で設計されていますが、サブセットとして用意されているに過ぎず、完全に互換性があるわけではありません。例えば、firstChildといったプロパティは用意されていないので、この場合は代わりにchildNodes[0]を使ってください。

MozillaのWeb Componentsへの関わり方

MozillaもWeb Componentsに対し、積極的な姿勢を見せています。FirefoxでもWeb Componentsの仕様のうち、いくつかが実験的に実装され、またX-TagやBrickといったWeb Componentsをつかったコンポーネント作成を後押しするライブラリもリリースしています。

X-Tag

X-TagはWeb Componentsの作成をサポートするライブラリです。webcomponents.jsをポリフィルとし、記述の簡略化や一元化いったライブラリとしての目的もPolymerと似ている部分がありますが、Polymerに比べて多くのブラウザをサポートしているという特徴があります。X-Tagのブラウザターゲットは以下の通りです。

以下は公式ドキュメント引用のX-Tagを使ったカスタム要素作成のコード例です。Polymerほど多機能ではない分、シンプルで全体像を把握しやすいかもしれません。

xtag.register('x-accordion', {
  // extend existing elements
  extends: 'div',
  lifecycle:{
    created: function(){
      // fired once at the time a component
      // is initially created or parsed
    },
    inserted: function(){
      // fired each time a component
      // is inserted into the DOM
    },
    removed: function(){
      // fired each time an element
      // is removed from DOM
    },
    attributeChanged: function(){
      // fired when attributes are set
    }
  },
  events: {
    'click:delegate(x-toggler)': function(){
      // activate a clicked toggler
    }
  },
  accessors: {
    'togglers': {
      get: function(){
        // return all toggler children
      },
      set: function(value){
        // set the toggler children
      }
    }
  },
  methods: {
    nextToggler: function(){
      // activate the next toggler
    },
    previousToggler: function(){
      // activate the previous toggler
    }
  }
});

Brick

X-Tagだけでなく、UIコンポーネント群として配布しているのがBrickです。PolymerとX-Tagとの関係になぞらえるなら、BrickはCore ElementsやPaper Elementsと同じような存在と言えます。以前まではX-Tagが使われていましたが、現在は非依存になっています。

BrickもPolymerの対抗馬として注目されていましたが、開発が中断されているのか、最近は更新がない状態です。参照しているポリフィルライブラリもwebcomponents.jsではなく、旧称のplatform.jsになっているので、利用する場合は注意してください。

Web Componentsの各仕様へのMozillaの対応

2014年12月の記事ですが、MozillaのWeb Componentsに対する興味深い記事が公開されました。

記事には、端的に述べると 「Custom ElementsとShadow DOMの実装は進め、HTML Importsの実装は見送ります。差し当たって、HTML Importsの機能はJavaScriptのライブラリでやってください。」 とあります。HTML Importsが実装されないことについての議論がコメント欄にて行われていますが、この記事の筆者であるAnne van Kesterenは以下のように述べています。

> Both ES6 modules and service workers open up resource dependency management to web developers. And while ES6 modules is mostly intended for JavaScript, we want to see what kind of dependency systems will be built on these two systems in libraries and frameworks before committing to a standardized design. Especially before committing to one that is not influenced by them. (ES6 modulesやService Workersも依存関係の解決に関わってくる。ES6 modulesはJavaScript向けのものだし、ライブラリやフレームワークにとってどのような機構になるかを見たい)

依存管理の仕組みが、HTML ImportsとES6 modules、Service Workerのような、Web WorkersのimportScriptsといった方法が複数存在してしまうことを懸念してのことと察します。例えばHTML ImportsとES6 modulesは目的を共有するものではないはずですが、HTML ImportsでロードするHTML内でES6 modulesが使われるようなケースも加味して、様子を見たいということでしょう。

Bosonic

BosonicもPolymerやX-Tag同様に、Web Componentsの作成を支援するライブラリです。

PolymerやX-Tagと異なるのは、これらのようにライブラリとして機能を補完したりするのではなく、作成したWeb ComponentsをBosonicを使って事前にビルドするトランスパイラであるという点です。Bosonicでビルドすることで、Internet ExplorerやSafariを含めたWeb Componentsに関するAPIが実装されていないブラウザでも機能するコードが生成されます。

Bosonicはブラウザターゲットは以下の通りです。

Bosonicで生成するコード例

以下は公式で例示されている<b-hello-world>というWeb ComponentsをBosonicを使ってビルドする例です。

<element name="b-hello-world">
  <style>
    :host {
      text-align: center;
      font-weight: bold;
      color: red;
    }
  </style>
  <template>
    <p>Hello world!</p>
  </template>
  <script>
    ({
      createdCallback: function() {
        var root = this.createShadowRoot();
        root.appendChild(this.template.content.cloneNode(true));
      }
    });
  </script>
</element>

Bosonicを使ってコンパイルすると、以下の様なCSSとJavaScriptのコードが出力されます。

b-hello-world {
  text-align: center;
  font-weight: bold;
  color: red;
}

(function () {
  var BHelloWorldPrototype = Object.create(HTMLElement.prototype, {
    createdCallback: {
      enumerable: true,
      value: function () {
        var root = this.createShadowRoot();
        root.appendChild(this.template.content.cloneNode(true));
      }
    }
  });
  window.BHelloWorld = document.registerElement('b-hello-world', { prototype: BHelloWorldPrototype });
  Object.defineProperty(BHelloWorld.prototype, '_super', {
    enumerable: false,
    writable: false,
    configurable: false,
    value: HTMLElement.prototype
  });
  Object.defineProperty(BHelloWorldPrototype, 'template', {
    get: function () {
      var fragment = document.createDocumentFragment();
      var div = fragment.appendChild(document.createElement('div'));
      div.innerHTML = ' <p>Hello world!</p> ';
      while (child = div.firstChild) {
        fragment.insertBefore(child, div);
      }
      fragment.removeChild(div);
      return { content: fragment };
    }
  });
}());

これらのファイルと、いくつかのポリフィル(HTMLImports、MutationObservers、WeakMap、Custom Elements)をHTMLでロードすることで、非対応のブラウザでも<b-hello-world>を使うことができます。

トランスパイラとして導入するメリット

新しいAPIで書かれたコードを、非対応のブラウザ向けに従来のAPIで実行できるようにするというのは、ES6をBabelでES5に変換するというアプローチと同様です。新しい技術の導入は常にブラウザサポートと天秤にかかり、サポートを優先するが故に新しい技術を試すことができないというジレンマも多いでしょう。しかし、こういったトランスパイラによって、新しい技術の導入と古いブラウザのサポートが、コストをかけずに両立できるのは非常に嬉しいところなのではないでしょうか。

<element>タグを使った古い記述であったり、リポジトリの更新もしばらくされていないなど、使うには少々抵抗があります。しかし、トランスパイラというアプローチは非常に興味深いものがありますので、メンテナンスが再開されるのを期待したいところです。

まとめ

Polymer 0.8のアップデート、X-Tag・Brick・Bosonic、およびFirefoxの実装状況について紹介しました。

ブラウザの実装状況は、Internet Explorer・Safariが未だに思わしくありません。仕様についても、FirefoxがHTML Importsの実装を遅らせたようにまだ確実とはいえない状況です。しかしこれは議論が行われている表れでもあり、Polymerの0.8がリリースされたことも含めて、Web Componentsに関する技術は普及に向けて着実に前進しているといえます。

しかし、Shadow DOMによってCSSやJSがカプセル化されても、今度はコンポーネント化のアプローチについて頭を悩ませることになるでしょう。Web Componentsが一般化するには仕様の安定やブラウザのサポートだけではなく、我々開発者でナレッジを蓄積していくことが最も重要です。