Polymerでpjaxする、またはapp-locationの使い方
この日記はPolymerで作っている、つまりWebコンポーネントを使っている。そのために表示が遅い。表示するまでに
- Webコンポーネントに必要なpolyfillを読み込む
- Polymerライブラリーを読み込む
- 各種カスタムエレメント定義をロードする
- JavaScriptで各種カスタムエレメント定義を実行する
- HTML中の各種カスタムエレメントを有効化する
というステップがあって、これを毎ページ繰り返すからだ。前々から何とかしたいなあとは思っていて、この連休で、サイト内リンクをpjaxにすることで少し改善させた。
各ステップはpjaxによって以下のように改善される。
目次
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-locationとiron-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-location
のroute
オブジェクトからpath
プロパティを取り出してセットしている(path
の他にhash
プロパティもあって、本当はこれもちゃんとハンドリングしないといけない)。
auto
属性をつけているとurl
属性が変わった際に自動でAjaxが行われるので、
リンクをタップ -> app-locationのroute属性変更 -> iron-ajaxのurl変更 -> Ajaxリクエスト
という流れをJavaScriptを書かずに実現してくれる(リクエストを間引くのも、使ってないけど、HTML属性によって定義できる)。
あとはレスポンス時やエラー時の処理をそれぞれtransit
、fallback
という関数としてJavaScriptで書いてやって出来上がりだ。transit
としてJavaScriptで書いた処理は、殆どtitle
要素とmain
要素の書き換えのみ。
終わりに
Polymerによるpjaxはこのようにして実現できる。これには、公式サイトの以下のページがとても参考になった。
» Routing with <app-route> - Polymer Project
余談だけど、Turbolinksを使うと自分で実装しなくてよかったのかも知れないなと思っている。
あと、今回、ページ内に一つblog-router
を置くことによって、つまり一元的なルーターを使ってpjaxを実現している。ReactやAngularでもルーターライブラリーがあるように、この手の処理は一元的なルーターでやるのが普通なのかも知れない。でも、次へリンクなどのHTML要素に結び付く形で、それがpjaxによる遷移かどうかを管理できるようにする、引いては、そのリンクのカスタムエレメントの機能としてpjax処理を実装できた方が、コンポーネント志向としてはいいのかも知れないなあと、やった後で思った。気が向いたらやってみるかも(そして、世界中のみんながルーターを一元的に作っている理由を知るのだ、きっと)。
今この記事書いてて気付いたけど、ページ内リンクが機能しなくなってしまった……。もう遅いので、後日の対応とします。(追記。直しました。)