Polymerを実際に使ってみた


今の現場で、いろいろあって（？）私の独断と偏見でPolymerの導入が許可されました。独断と偏見なので特に誰に反対されたというわけでもないのですが。

いかんせん実践で利用しているという日本語での情報がほとんどなく、公式ページとにらめっこしながら少し使ってみました。日本語情報も去年Polymerが発表された段階とかだったりして、細かかったり細かくないところがいろいろ変更になっていますので、それもあわせて書いてみます。

## Polymerのインストール
Polymerはplatform.jsというpolyfill、サポートされていない環境でも擬似的にWebComponentsの各技術要素を利用可能にするライブラリと、その上で実装されたPolymerライブラリがまとめられたものです。

http://www.polymer-project.org

詳しくは公式をみてもらった方が早いです。Shadow DOMとかHTML importsとか、そういった要素の実装はすべてplatform.jsが担っているので、本当にWeb Componentsの先行実装といえるのはplatform.js、ということのようです。

インストール方法としては、基本的にはbowerを利用するようです。私もプロジェクトで使っているので、そのままbowerを利用するようにします。

```shell
$ bower install --save Polymer/polymer
```

基本的にはこれだけです。polymerの依存としてplatform.jsも同時に入ります。

## 一番簡単なPolymer Elementの作成
```html
<link rel="import" href="bower_components/polymer/polymer.html">
<polymer-element name="x-test">
  <template>
    This is <b>{{owner}}</b>'s name-tag element.
  </template>
  <script>
    Polymer('x-test', {
    // initialize the element's model
    ready: function() {
    this.owner = 'Rafael';
    }
    });
  </script>
</polymer-element>
```

公式に乗ってるサンプルほぼそのまんまです。polymer-elementのname属性には、必ず **-を含んだ名前** をしているする必要があるので注意です。

後はこれをHTML importsで読み込むことで、x-testという要素が利用できます。

と、ここまでは他の試してみたりした記事でも書かれている内容です。ここからは、実際に試してみて発見した問題とか開発環境とかについて書いていきます。

## 外部ライブラリのスコープ問題
Web Componentsを利用する上で最も気になるのが、サードパーティのライブラリを使ったときのグローバルスコープです。

Polymerが基本的なWeb Componentsのセットとして公開している[Core Components](http://www.polymer-project.org/docs/elements/core-elements.html)というのがありますが、これらも特にサードパーティライブラリは使っていません。AjaxとかもXHRオブジェクトをそのまま扱うための要素を新しく作成して、それを利用するようです。

さて、結論からすると、これは **現状解決手段がありません。**　無論、大きな問題として認識されているようではありますが、Web Components(本当はPolymer ElementですがここではWeb Componentsで統一します）内で、サードパーティライブラリ、例えばjQueryとかUnderscoreをscriptタグでロードすると、もれなくそれまで読み込まれていたものを上書きします。これは、Web Componentsのscriptタグを解釈するときのthisが、 **呼び出す側のwindowオブジェクト** であることに起因するようです。

なお、この挙動は一番Web Componentsの実装が進んでいるChromeでも同様だったので、そのままWeb Componentsの制限となりそうです。

さて、となるとこれに対する何らかの解決法が必要です。さすがに、jQueryとかUnderscoreレベルの関数とかがさっくり作れるとも思わないし、再発名も甚だしいので。

一応、解決方法としてはいくつか提案されていました。

- ライブラリをラップしただけのWeb Componentsを作成し、それを各コンポーネントでHTML importでインポートする
  - ただし、この方法でもグローバル汚染を避けることはできません。同じライブラリを読み込むことを避ける、というだけです。
- Require.jsを利用する
  - 現実解としてはこれかと思います。ただし、これを利用するということは、サードパーティのWeb Componentsをそのまま利用する、ということができない可能性があります。

今回は、外部のWeb Componentsを利用する訳でもないので、とりあえず二番目でためしてみました。

### Require.jsとPolymerを併せて利用する
すでにやられている方がいたので、これの内容を拝借しました。

http://jsbin.com/efojap/2/edit

やっていることは、Require.jsを読み込んで、Polymerと組み合わせた関数を提供するだけのWeb Componentsを作成して、それをおのおので利用する、という感じです。

ただし、プロジェクトで既にRequire.jsを利用しているので、Require.jsを読み込まずに、Polymerとのglueコードのみを書くことにしました。

## Web Componentsをまとめる
Web Componentsは、それぞれ非同期での読み込みが行われるため、そのままだとコネクションの個数が飛躍的に増えてしまいます。特に、ある程度の規模で、複数のWeb Componentsをimportしているようなコンポーネントの場合、一つ読み込むだけで無視できない量になってしまいます。

それを避けるため、Polymerでは Vulcanizeというツールを提供しています。これは、Web ComponentsのHTMLとCSS、Scriptをすべて一つ乃至二つのファイルにパッケージ化してくれるツールです。実際には、Grunt/Gulpのプラグインが作成されているので、これを利用するのがよいかと思います。

grunt-vulcanize
https://www.npmjs.org/package/grunt-vulcanize

プロジェクトではload-grunt-configプラグインを導入していたので、vulcanize.jsというファイル名で以下のようなタスクを作成しました。

```javascript

module.exports = function(grunt) {
  function component(name) {
    var obj = {};
    obj.output = 'war/scripts/components/' + name + '.html';
    obj.input = 'components/' + name + '/index.html';
    var path = name.split('/');
    if (path.length > 0) {
      path = grunt.util._.rest(path.reverse()).reverse();
    }
    grunt.file.mkdir('war/scripts/components/' + path.join('/'));
    return obj;
  }
  var components = [
    component('hearing-aggregator')
  ];
  var files = {};
  components.forEach(function(component) {
    files[component.output] = component.input;
  });
  return {
    'default' : {
      options : {
        csp : true,
        excludes : {
          imports : [
            "polymer.html"
          ]
        }
      },
      files : files
    }
  };
};
```
実際にはもうちょっと工夫して、あるディレクトリ配下について、ディレクトリ単位でvulcanizeしたhtmlを自動的に出力するようにしています。後、Web Componentsを作成するディレクトリと出力するディレクトリは、他のアセットよろしく分けておいた方が無難です。

これで、とりあえずWeb Componentsを作成していく目処は立ちました。が、まだ問題がありました。

## CSSがよくわからない
Web ComponentsでCSSを利用する最大のメリットは、スコープの問題から解放される、という点が最大のメリットだと思われますが、現状Chrome以外ではそれに対する実装 - Shadow DOM - がほぼ無い状態です。そのため、Polymerが黒魔術を駆使して、そういったCSSでもなんとか使えるようにしてくれています。

プロジェクトでは、すでにSass/Scssを全面導入していたため、今更PureなCSSを書く気にもなれなかったので、Sass/Scssのコンパイルが前提です。

CSSの書き方としては、Polymerとしては大きく三種類あるようです。

1. :host <selector> な書き方
Nativeでshadow domがサポートされた場合、この書き方が通常になる。現時点（2014/7）でも、Chromeならばこの形式でばっちり効く。
Polyfillが効いている場合、こういう書き方がされたものは、他も含めてplatform.jsのほうで、headタグの先頭にstyleが持っていかれて、:hostの部分がタグ名に変更される。

2. polyfill-next-selectorを利用する
これでも大丈夫っぽいんだけど、どうも本当に直後（隙間があるとアウト）の様で、Sass/ScssでコンパイルされたCSSだと効かなかった・・・

3. polyfill-ruleを利用する
これもなぜか効かなかった・・・結構記述が面倒なのと、:afterとかに関して記述できない（contentが使われている）と思われるので、あまり利用シーンはないと思われる。公式でもこれが必要なのはレアケースだ、って言ってるし。

Core Elementsを眺めてみても、それぞれことなる形式だったりしましたが、とりあえず１番目の手法を利用することにしました。これならSass/Scssの記法でもちゃんと出力されます。

なお、Sass/Scssのコンパイルは、Vulcanizeの前にやっておく必要があります。Vulcanizeされ、CSSをlinkタグで読み込んでいる場合、生成されたHTMLに埋め込まれるため、先に生成しておく必要があります。

## とりあえず
これくらいやって、とりあえず作成していく目処が立ちました。他には、watchして自動的にvulcanizeする、だとかそういったものも必要ですが、とりあえずはこれで何とかなります。

Polymerには他にもData Bindingとかいろいろ機能があるので、その辺でまた地雷を踏み抜くかもしれませんが、仕事でこういったエッジの技術に触れることもあまり無いと思うので、いい機会と思っていろいろやってみようかと思います。