ジンジャー研究室

長めのつぶやき。難しいことは書きません。

Polymerで個人のホームページを構築してみた

Web ComponentsでWebサイトの作り方が変わる

ブログやSNSの登場以来ずっと放置していた自分のサイトを復活させてみた。(祝!)
一応このブログは技術ブログのような顔をしているので、コンテンツの中身についてはあまり言及しない代わりにアーキテクチャについて少し喋ろうかなと思う。


で、今回はPolymerというWeb Componentsのpolyfill+αなライブラリを使ってみたので感想を書いてみた。
Polymerというライブラリ自体の性能とか導入可能性について言いたいわけではなくて、あくまでWeb Componentsを使うとこんな感じでサイト構築できるんじゃないかというところ。

完成したページ


http://jinjor.herokuapp.com/

World Maker
世界の創造主を名乗って自作の音楽を公開。かれこれ11周年くらい。

Polymerを使ったカスタム要素の実例

Ploymer主にを使用した場所。

  1. 音楽プレイヤー
  2. 音量ツマミ
  3. 全体のレイアウト

以降、順番に見ていく。

音楽プレイヤー

YouTubeっぽい自作のプレイヤー(上のサムネイル画像の下の方)をカスタム要素化。
Web Componentsの一番オーソドックスな使い方だと思う。今まではこういったコンポーネント部品を共通化する方法がなくサーバサイドのロジックでこしらえていたのだが、それをクライアントサイドで独立して出来るようになったというのがポイント。
つまり、サーバサイドで動的なデータを扱う必要がなければ、静的なファイルを配信するだけで良い。実際に今回のサイト構築はgrunt-contrib-connectというモックサーバだけで事足りた。

以下は、カスタム要素の呼び出し元のコード。

<link rel="import" href="x-midi-viewer.html"></link>

...

<x-midi-viewer audio="music/2014/sakura.mp3" smf="music/2014/sakura.mid" delay="1500"></x-midi-viewer>

音楽ファイルとしてMP3ファイルとMIDI(SMF)ファイルを用意する。delayは両者のタイミングのずれを補正するもの。カスタム要素は事前にインポートしておく。
呼び出し先は長いので省略。

音量ツマミ

f:id:jinjor:20140224032516p:plain

もう少し凝った使い方として、子コンポーネントとモデル(≒可変なデータオブジェクト)を共有するといった事が出来る。
DOMに慣れていると「子コンポーネントからクリックイベントが飛んできたら目的のオブジェクトを更新して…」というアプローチになりがちだが、ここでは更新して欲しいオブジェクトを直接渡してしまうことにした。

呼び出し元。

<x-volume-controller gain="{{sequencer.gainNode}}"></x-volume-controller>

ここでgainNodeというのは、Web Audio APIに定義されているGainNodeを実装したインスタンスで、これを渡してやることでカスタム要素に音量の制御を委譲している。

呼び出し先の一部を抜粋。

<polymer-element name="x-volume-controller" attributes="gain"><!-- ★ -->
  <template>
    <style>
      ...
    </style>
    <button class="volume_icon" data-value="{{loudness}}"></button>
    <button class="volume_slider" on-mousedown="{{startMoving}}" on-mousemove="{{move}}">
      <div class="volume_slider_foreground" style="left:{{left}}px"></div>
    </button>
  </template>
  <script>
    Polymer('x-volume-controller', {
      ready: function() {
        ...
      },
      move: function(e){
        var self = this;
        var x = e.pageX - e.currentTarget.offsetLeft;
        if(self.moving){
          var left = Math.max(Math.min(x, 46),0);
          self.left = left - (firefox ? 3 : 0);
          self.gain.gain.value = left / 46;// ☆
          self.updateLoudness();
        }
      },
      ...
    });
  </script>
</polymer-element>

<template>と対応する<script>を合わせて記述する。
Polymer関数の第2引数は所謂ViewModelになっていて、このオブジェクトの状態がテンプレートに自動手反映される。また、テンプレートはイベントと関数との関連付けも同時に担う。
スライダーを動かしたときに音量調節する処理をmoveメソッドに記述している。★で受け取ったGainNodeを☆で操作。所々怪しげなコードが書いてあるが気にしたら負け。readyは初期化時に呼ばれる。

全体のレイアウト

メインフレームの外(=メニュー部分)を共通化したい場合の新しい選択肢。つまりこう。

  • インラインフレーム(一部を再描画)
  • サーバサイドで共通化(全体を再描画)
  • Ajaxで中身を書き換える(一部を再描画)
  • クライアントサイドで共通化(全体を再描画)← New

汎用部品だけではなく、アプリケーションの責務分離や共通化のためにWeb Componentsを使うのもありなのではないかと思う。

呼び出し元のコード。

  <body>
    <x-layout>
      <div>
      <h3>リニューアルしました</h3>
        ...
      </div>
    </x-layout>
  </body>

今までと違うのは、カスタム要素にコンテンツを丸ごと渡している点。

呼び出し先のコード。

<link rel="import" href="x-social.html">
<polymer-element name="x-layout" noscript>
  <template>
    <style>
      ...
    </style>
    <div id="container"><div>
      <x-social></x-social>
      <header>
        <a href="/"><h1>World Maker</h1></a>
        <p>音楽とプログラミングを主食にするジンジャーのホームページです</p>
      </header>
      <nav>
        <ul>
          ...
        </ul>
      </nav>
      <div id="main-contents">
        <div>
          <content><!-- ★ここに挿入される -->
          </content>
        </div>
      </div>
    </div></div>
  </template>
</polymer-element>

渡されたコンテンツは<content></content>の位置に挿入される。(複数の場合など、更に細かく指定することも出来る。)

ハマったところ

現時点で未解決の課題。単なる調査不足とも言う。

ページの構築が遅い

動的にロードしているのである程度は仕方ないのだが、従来のAjax同様に最初にページが表示されてからガクガクっと画面が動く。
今回は対策方法が見つからなかったので、とりあえず放置。結構簡単なページでもこれなのでレイアウト用に使うのはあまり向いていなかったりするのだろうか。

メタ情報などを共通化できなかった

これもレイアウト関連。Open Graphを使う際に<meta>タグを挿入する必要があったのだが、<head>から離れていたためにカスタム要素で上手く共通化することが出来なかった。

カスタム要素内のスタイルを外部ファイルに切り出せない

カスタム要素の<style>内に記述する必要がある。これがPolymer独自の制限なのかどうかは分かっていない。

カスタム要素内のid等の扱い

ハマったというより勉強不足できちんと理解していないので不安を募らせているところ。
例えばカスタム要素内のidは、次のようにして取り出すことが出来る。

<div id="screen"></div>
...
<script>
Polymer('x-midi-viewer', {
      ready: function() {
        var el = this.$.screen;
...
</script>

このidは外界のそれとは区別されるので、document#getElementByIdで取得することが出来ない。
で、今回はその辺の細かい仕様はなんとなくの理解で済ませていたのだが、大規模開発でコーナーケースにあたっても混乱無く出来るか自信は無い。あとjQueryなど既存のライブラリが不自由なく使えるかどうか。

Web Componentsと微妙に仕様が異なる

例えば、ViewModelのバインディングなどはPolymer独自の仕様。どこに差異があるのかをある程度把握しておかないと、すんなり移行というわけには行かなそう。
また、要素構築時のコールバックの一部が動かないなどの不具合があった。

最後に

いくつか不安要素はあったものの、現時点でもある程度のものは普通に作れるので期待して良いと思う。

関連する去年の記事。もし興味があれば。
GUIをツリーにする話(前篇) - ジンジャー研究室
GUIをツリーにする話(後篇) - ジンジャー研究室
GUIをツリーにする話(新篇) - ジンジャー研究室


以上!