アペフチ

Polymerでpjaxする、またはapp-locationの使い方

この日記はPolymerで作っている、つまりWebコンポーネントを使っている。そのために表示が遅い。表示するまでに

  1. Webコンポーネントに必要なpolyfillを読み込む
  2. Polymerライブラリーを読み込む
  3. 各種カスタムエレメント定義をロードする
  4. JavaScriptで各種カスタムエレメント定義を実行する
  5. HTML中の各種カスタムエレメントを有効化する

というステップがあって、これを毎ページ繰り返すからだ。前々から何とかしたいなあとは思っていて、この連休で、サイト内リンクをpjaxにすることで少し改善させた。

各ステップはpjaxによって以下のように改善される。

目次

  1. pjaxとは
  2. polyfill読み込み
  3. Polymerライブラリーの読み込み
  4. カスタムエレメント読み込み
  5. カスタムエレメント定義の実行
  6. Polymerでpjax
    1. app-location
    2. iron-ajax
  7. 終わりに

pjaxとは

有名なので不要だとは思うけど、一応pjaxを説明しておく。

pjaxは、

  • Ajaxによる画面遷移
  • locationオブジェクト(アドレスバーのURL)の書き換え

の組み合わせだ。サイト内の別ページへのリンクをタップした際に、通常のブラウザーの画面遷移をする代わりに、JavaScriptでリンク先のHTMLを取得して、現在のページと置き換える。今回は、title要素とmain要素を置き換えることで、画面遷移としている。ページ全体でなく、一部の書き換え・更新にもよく使われる。

同時にlocationを書き換えることで、ブラウザーの進む/戻る・リロード、アドレスバーからURLをコピーしての共有など、通常の画面遷移であればできていることを可能にしている。

後者のためにJavaScriptのpushState機能を使っていることからpjaxと名付けられている:
defunkt/jquery-pjax: pushState + ajax = pjax

pjaxは「現在のDOMツリー内での置き換え」が機能なので、外から飛んできて最初に表示するページでは役に立たない。

polyfill読み込み

Webコンポーネントはまだ策定中・ブラウザー実装途中の仕様なので、クロスブラウザーでは動かない。具体的には、Chromeでしか全部は動かない。そこで、他のブラウザーでも動くよう、polyfillやshimを読み込む必要がある。

これにはWebコンポーネント仕様の一部であるカスタムエレメントなど以外にも、URLコンストラクターやPromiseなどのpolyfillも含まれる。サーバー側でブラウザーの判定などはしていないので(GitHub Pagesなのでそもそもできない)、Chromeのように不要であっても読み込んでいる。

こういうのは普通、libs.jsのような一つのファイルにまとめることでリクエスト回数を減らすものだけど、面倒くさくて後回しにしている(後回しにするうちにGitHub PagesでHTTP/2が使えるようになるといいなあ、という期待もちょっとある)。

pjaxによって、mainの外にあるscriptの読み込みと実行がスキップされるので、パフォーマンスがよくなっている。あと、そもそも同じscriptを二回読み込んだりすると、イベントリスナーの登録が複数回行われたりして意図しない動作になりがちなので、基本的にscriptはpjaxでの置き換え対象に入れたくない。

Polymerライブラリーの読み込み

ページをPolymerで作っている以上、当然Polymerを読み込む必要がある。

これもJavaScriptの読み込みなので、上と同じくpjaxによってスキップし、パフォーマンスを向上させている。

カスタムエレメント読み込み

Polymerが提供していてマテリアルデザインを実現するのに便利なPaper Elementsや自作の物など、各種カスタムエレメントは通常一つのHTMLファイルになっている。その中に、HTMLタグの他CSS宣言や要素定義のJavaScriptを書くようになっているし、僕もそうしている。一つのカスタムエレメントが複数のカスタムエレメントの組み合わせであることもよくあって、依存エレメントの分HTMLを読み込む必要があるのが普通だ。

先のJavaScriptライブラリーとは違って、これはさすがにリクエストが多くなり過ぎるのでvulcanizeによって一つのファイルにまとめている。その一つにまとめたHTMLファイルはhead要素中のlink要素

<link href="components/elements.vulcanized.html" rel="import" />

によって読み込んでいる(実際にはこのタグを書き出すヘルパーがMiddleman Web Componentsにあるので、それを使っている)。

これもmain要素の外にあるので、pjaxによってスキップしている。

カスタムエレメント定義の実行

カスタムエレメントは、単にHTML中に<paper-card>などタグを書くだけでは有効にならない(知らないタグとして扱われる)。このタグがカスタムエレメントの物であることをブラウザーに知らせ、各種機能を定義するにはJavaScriptを使う必要がある(参考:Custom Elements v1: Reusable Web Components)。

この定義は、上のelements.vulcanized.htmlに書かれているので、pjaxによってやはりこのステップもスキップできる。

これまでのステップは、(HTTPヘッダーやService Workerなどで)キャッシュを上手に使うことでも飛ばせるのだけど、カスタムエレメント定義は、ファイルを読み込んだの処理なので、ページ遷移ごとに毎回実行する必要があり、キャッシュできない。なのでここが、キャッシュ機構を入れてもなおpjaxが活きるところだと思う。

本当はpolyfillやPolymer読み込みをキャッシュしても、同様にJavaScript実行はページ表示毎に発生するのだけど、カスタムエレメント定義は特に多くなりがちなので特別に節を設けた。

カスタムエレメントの有効化

カスタムエレメント定義が終わったらブラウザーは、HTML中のカスタムエレメントタグ(に相当するDOMノード)を、そのカスタムエレメントとして扱い始める。このステップはpjaxではスキップできない。

Polymerでpjax

ようやく本題だけど、今回のpjax実装では、Polymerが提供しているapp-locationiron-ajaxというカスタムエレメントを使って実現してみた(blog-router.html)。pjaxは普通、全部JavaScriptでやるものだと思うけど、半分くらいの処理はHTMLタグを書くことで実現できてしまっていて、不思議な感じがした。

app-location

app-locationを使うと、サイト内リンクが全部無効化される。その代わり、イベントリスナーでクリックイベントをハンドリングしたり、リンクに関する情報をデータバインディングを使って別の要素に渡したりできる。

今回は

<app-location url-space-regex$="[[baseRegex]]" route="{{route}}" id=location></app-location>

routeというオブジェクトに、画面遷移に関する情報を入れることにした。

{{...}}はPolymerの提供するデータバインディング用の記法で、他に{{route}}と書かれた場所と連動する(Data binding - Polymer Project)。

iron-ajax

app-locationは、アドレスバーのURLの書き換えはしてくれるけど、実際のリクエストは投げてくれない。ので、それをJavaScriptでイベントハンドラーとして書くか、今回のように別の要素と連携させないと意味がない。

iron-ajaxはその名の通りAjaxしてくれるカスタムエレメントで、画面上にレンダリングはされない。純粋にJavaScript的な実行のためにある。これが要素になっているが不思議な感じがする。

これもデータバインディングの記法を使いつつ

<iron-ajax url="{{route.path}}" handle-as=document auto on-response=transit on-error=fallback id=ajax></iron-ajax>

と書くことで、app-locationrouteオブジェクトからpathプロパティを取り出してセットしている(pathの他にhashプロパティもあって、本当はこれもちゃんとハンドリングしないといけない)。

auto属性をつけているとurl属性が変わった際に自動でAjaxが行われるので、

リンクをタップ -> app-locationのroute属性変更 -> iron-ajaxのurl変更 -> Ajaxリクエスト

という流れをJavaScriptを書かずに実現してくれる(リクエストを間引くのも、使ってないけど、HTML属性によって定義できる)。

あとはレスポンス時やエラー時の処理をそれぞれtransitfallbackという関数としてJavaScriptで書いてやって出来上がりだ。transitとしてJavaScriptで書いた処理は、殆どtitle要素とmain要素の書き換えのみ。

終わりに

Polymerによるpjaxはこのようにして実現できる。これには、公式サイトの以下のページがとても参考になった。

» Routing with <app-route> - Polymer Project

余談だけど、Turbolinksを使うと自分で実装しなくてよかったのかも知れないなと思っている。

あと、今回、ページ内に一つblog-routerを置くことによって、つまり一元的なルーターを使ってpjaxを実現している。ReactやAngularでもルーターライブラリーがあるように、この手の処理は一元的なルーターでやるのが普通なのかも知れない。でも、次へリンクなどのHTML要素に結び付く形で、それがpjaxによる遷移かどうかを管理できるようにする、引いては、そのリンクのカスタムエレメントの機能としてpjax処理を実装できた方が、コンポーネント志向としてはいいのかも知れないなあと、やった後で思った。気が向いたらやってみるかも(そして、世界中のみんながルーターを一元的に作っている理由を知るのだ、きっと)。

今この記事書いてて気付いたけど、ページ内リンクが機能しなくなってしまった……。もう遅いので、後日の対応とします。(追記。直しました。)