Web Components を支えるPolyfillライブラリ

Web Components を今つかえる技術にするライブラリたち

ご存じの通り、Web Components はまだ仕様策定の途上段階(Templatesとかもうやることなさそうだけど)にあり、ブラウザの実装もまだまだこれからの段階。

「憎まれっ子世にはばかる」がソフトウェアにも適用される格言であったことを噛みしめている今、Web Components がブラウザネイティブでサポートされるのは、それなりに遠い未来になりそう。

Polyfill

幸い、Web Components自体、既存のウェブ開発で利用されていたスキームを、標準仕様としてデザインし直している趣なので、JavaScript で同様の振る舞いを再現することは可能だ。

そんなWeb Componentsを支えるPolyfill (新しい環境を基準にして、古い環境ではそれをエミュレートして差を埋める) ライブラリを、WebComponents.org で紹介されている3つについて。

Polymer - 有名&データバインディングetc付き

Welcome - Polymer

Polymer is a library that uses the latest web technologies to let you create custom HTML elements. Build anything from a button to a complete application as an encapsulated, reusable element that works across desktop and mobile. Welcome - Polymer

言わずもがなGoogle先生が提供しているWeb ComponentsのPollyfill + α盛りだくさんのライブラリ。

Core

Polymerは後述する Platformが優れたWeb Components関連のPolyfillであると同時に、Coreが便利機能を備えたラッパーになっている。

自分も <x-ahomu> という大変どうでもよい要素をPolymerで作ったことがある。以下は、雰囲気を感じるための適当な要素のサンプル。

<polymer-element name="x-foo">
  <template>
  <!-- shadow DOM here -->
  <span>(๑˃̵ᴗ˂̵)و < {{greeting}}</span>
  <ul>
    <template repeat="{{ face in faces | sayHi }}">
    <li>{{face}}</li>
    </template>
  </ul>
  </template>
  <script>
    Polymer('x-foo', {
      faces   : [],
      message : 'Hello!',
      get greeting() {
        return this.message + ' World!';
      },
      created: function() {
        this.faces = [
          '⁽⁽٩(๑˃̶͈̀ ᗨ ˂̶͈́)۶⁾⁾ ',
          '(੭ ˃̣̣̥ ω˂̣̣̥)੭ु⁾⁾',
          '( ๑˃̶ ॣꇴ ॣ˂̶)♪⁺',
          '(๑˃́ꇴ˂̀๑)'
        ];
      },
      attributeChanged: function(attrName, oldVal, newVal) {
        console.log(attrName, 'old: ' + oldVal, 'new:', newVal);
      },
      sayHi: function(ary) {
        return ary.map(function(str) {
          return str + ' < Hi!';
        });
      }
    });
  </script>
</polymer-element>

ご覧の通りといってはなんだが、データバインディングやフィルタといったAngularJSで見たことがあるような機構を備えている。

全容は、以下のリファレンス周りのサンプルコードに目を通せば察しがつく。機能は多いが、それほど複雑ではない印象。

ちなみにRuntime configurationを見ると、デバッグ系の実行時設定もある。Platform側に設定するけど、Polymer-Coreと一緒に読み込んだときだけの特典。

Web ComponentsをConcatする(requirejsでいうr.jsみたいなの)は、Concatenating Web Components with Vulcanize - Polymerで解説されているPolymer/vulcanizeを使うことができる。require or imports って感じがしてくる。

Elements

コンポーネント要素を作る機能だけでなく、汎用性のある既製品の要素も用意されている。Using elements - Polymer が使い方で、Docs が一覧。

おもしろいのはUI要素だけでなく、非UI要素もコンポーネント化している点。下記は、<core-ajax> がyoutubeのAPIをAjaxで叩いて、handleResponseというハンドラに結果を渡している例。

<core-ajax
    auto
    url="http://gdata.youtube.com/feeds/api/videos/"
    params='{"alt":"json", "q":"chrome"}'
    handleAs="json"
    on-core-response="{{handleResponse}}"></core-ajax>

ちなみにこういうディレクティブは、AngularJSでも作ることができる。Controllerに生やしたハンドラをAjax要素に渡すだけになるので、JS側から非同期コードを消すことができる。

JSONをとってきて単純に表示するだけ、といった要素ならHTMLに <ajax> を置くだけで実現できるという点もインパクトがあるのではないかと。<ajax> は仕事コードでも使っているが、慣れれば違和感ない。

PolymerLabsにもPolymer実装が埋まってるが、こちらはElementsみたいに正規ポジションを与えらてはいない風。

Platform

The Platform - Polymer

platform.jsにはWeb Componentsの主要仕様である、Shadow DOM, HTML Imports, Custom Elementsのpollyfillなどその他色々を内包している。このあとの他の2つをみた結果、結局PolymerのPolyfillが一番手堅い雰囲気だった。

1点だけあれー?ってなったのは...

という件だった。 _currentScript ね。

X-Tag - Custom Elementsを便利に

X-Tag - Web Components Custom Element Polylib

Creating custom elements has never been easier. X-Tag provides several powerful features that streamline element creation such as: Custom events and delegation, mixins, accessors, component lifecycle functions, pseudos and more. X-Tag Technology Stack - X-Tag - Web Components Custom Element Polylib

X-TagはCustom Elements周りをいいかんじにラップして、アクセサとかイベントデリゲーション、mixinなどを提供することを主な目的としている。

X-Tag & Polymer Now Share Web Component Polyfills を見ると分かりますが、X-Tagは途中からPolymerのPolyfillを取り込んでいる。

Shadow DOMのPolyfillはコストと目的(Custom Elementsを便利に使う)の都合で省略されていて、MDV(Model Driven View)は検討中とのこと。・・・2013/06/15付けの話なのでアレ。リポジトリには動きあるけどウーン。

下記、X-Tag - Web Components Custom Element Polylib からの転載だけど、やりたいことは分かりやすい。

xtag.register('x-accordion', {
  // extend existing elements
  extends: 'div',
  lifecycle:{
    created: function(){
      // fired once at the time a component
      // is initially created or parsed
    },
    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
    }
  }
});

Polymerと同様に、BackboneとかVueみたいな感じでオブジェクトにどんどん記述して、内容を定義していくスタイル。考えようによっては、Data BindingやFilterring expressionに頼らない分、AngularJSが嫌いなひとでも好きになれるかも。

Bosonic - ビルドプロセス型

Bosonic Web Component

Bosonic's philosophy is to enable developers to build Web Components as close as possible to the current spec. Therefore, Bosonic aims to not become a dependency: ideally, when browsers will implement all Web Components spec, you'll be able to drop Bosonic and to use your components directly. Documentation - Bosonic Web Component

...と書いてあるからピュアなものに期待したけど、薄いラッパーのためにyeomanビルドプロセスを抱えている分うーん(´-`;)

BosonicもCustom Elements, Mutation Observers, Weak Maps についてはPolymerのpolyfillを流用しているようだ。あとShadow DOMの自前polyfill(未確認)

npm install -g yo generator-bosonic
yo bosonic

yeomanで generator-bosonic を入れてscaffoldする。

<element name="b-any-world">
  <style>
    ::host h3 {
      color: red;
    }
  </style>
  <template>
    <h3><content></content>, world!</h3>
    <p>Lorem ipsum</p>
  </template>
  <script>
    ({
      createdCallback: function() {
        var root = this.createShadowRoot();
        root.appendChild(this.template.content.cloneNode(true));
      }
    });
  </script>
</element>

このHTMLをscaffoldしたときに含まれている、grunt-bosonic タスクを通して、次のようなJSとCSSに変換します。ここでの記法自体は、bosonicオリジナルなテイスト。

b-any-world h3 {
  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-any-world', { prototype: BHelloWorldPrototype });
  Object.defineProperty(BHelloWorldPrototype, 'template', {
    get: function () {
      var fragment = document.createDocumentFragment();
      var div = fragment.appendChild(document.createElement('div'));
      div.innerHTML = ' <h3><content></content>, world!</h3> <p>Lorem ipsum</p> ';
      while (child = div.firstChild) {
        fragment.insertBefore(child, div);
      }
      fragment.removeChild(div);
      return { content: fragment };
    }
  });
}());

そいで、使いたいページでこれらのCSSとJSが読み込まれるようにすればOKというノリ

<!DOCTYPE html>
<html>
<head>
    <title>Demo</title>
    <meta charset="utf-8">
    <script src="js/bosonic-polyfills.js"></script>
    <script src="js/b-any-world.js"></script>
    <link href="css/b-any-world.css" rel="stylesheet">
</head>
<body>
    <b-any-world>Hello</b-any-world>
</body>

生成されるJSそのものはピュアな感じなので、たしかにbosonicを捨ててもWeb Componentsとして成り立ちそうではある。

瑣末だが、この記事を書いてる時点で試したらWeb Platform featuresのフラグをEnableにしたCanaryで、bosonic-pollyfillsがエラー吐いてる...

余談、実はえらいかも

結論

個人的にはふつうのPolyfillとして動いてくれるものを精査したかったのだけど、結果的に Polymer/platform を使えば問題なさそう。Polymer以外も同じPolyfillを使っているので、実質的にWeb Componentsを支えているのはplatform.jsさんといっても過言では無さそう。

Googleさん、Web Componentsの発展を願うついでにPolymerを売り込んでるので、なんというかステルスロックイン感がある。

細かい誤りとか補足あれば随時編集予定