Pjax に挑戦したら思っていた以上に苦労した話

GitHub が採用している、非同期でぬるぬる動く画面遷移、これ pushState と Ajax を組み合わせたテクニックで実現されているんですね。その名も Pjax。

HTML5 の history.pushState を使うからブラウザの履歴にも対応でき、しかも URL がキレイ。Pjax についての詳細な説明は下記のエントリが参考になりました。

Pjax 始まったな。

           |i
     \      |.|
      ト\   /| ト
      | トヽ   / | | ト
      | | トヽ\/| | | ト    /
      | | | ト\≧三ミゞ=イ/
     ム彡''´ ̄ ̄    ̄ ヽ{__..
    /             V´
    ノ  __          ',
 ,. == y ̄, __、\_        )      世 界 的 で す も ん ね
 |i  }-| ゝ二 |/ ̄ ̄  /ニ,l
 ヽ__ノ/ヾ _ ノ       > }}
  / >≦'__        し /        乗 る し か な い
   Vて二オカ       (_,/}
   Yこ二ノ!!|          }         こ の ビッ グ ウ ェ ー ブ に
    Y⌒ 从        ∠)
    从从从トミ   _.ィニ二 ̄丶
     ミ三三彡 ' ´      \ \
        /           \ヽ
      /            ミ;,. ', ',
       |   _  _ __    \',.',
      ノ!   | V7\ ´/
     / l /_ゝ| ト >__/ /
     |   ヽン ´  ヽー'
    i|                l
    |:! ヽ              |
    | ト、 `ミ,            l


さっそく、Github から最新の jquery.pjax を入手して試してみます。


Web アプリは App Engine 向けに作ることが多いので、今回も当然のように App Engine/Python + Kay Framework でサンプルを作成しました。

# -*- coding: utf-8 -*-
"""
core.views
"""
from datetime import datetime
from kay.utils import render_to_response, get_response_cls

def index(request, name=None):
    return greet(request)

def greet(request, name="world"):
    message = "Hello %s!" % name

    # Pjax のリクエストはヘッダに X-PJAX がついている
    if "X-PJAX" in request.headers:
        # Pjax のときはコンテンツだけを返す
        return get_response_cls()("Hello %s!" % name)
    else:
        # Pjax じゃないときはページ全体を返す
        data = dict(
            now=datetime.now(),
            message=message)
        return render_to_response('core/index.html', data)
# -*- coding: utf-8 -*-
# core.urls
# 

from kay.routing import (
    ViewGroup, Rule
)

view_groups = [
    ViewGroup(
        Rule('/', endpoint='index', view='core.views.index'),
        Rule('/greet/<name>', endpoint='greet', view='core.views.greet'),
    )
]
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Pjax Test</title>
    </head>
    <body>
        <div id="header">
            <h1>Pjax Test({{ now }})</h1>
        </div>
        <div id="navi">
            <!--pjax を適用するアンカータグにクラス js-pjax を指定-->
            <a class="js-pjax" href="{{ url_for('core/index') }}">index</a>
            <a class="js-pjax" href="{{ url_for('core/greet', name='foo') }}">foo</a>
            <a class="js-pjax" href="{{ url_for('core/greet', name='bar') }}">bar</a>
        </div>
        <div id="main">
            <!--ここが書き変えられる-->
            {{ message }}
        </div>

        <script type="text/javascript" src="{{ media_url }}/js/jquery-1.6.2.js"></script>
        <script type="text/javascript" src="{{ media_url }}/js/jquery.pjax.js"></script>
        <script type="text/javascript">
            $(function() {
                // PJax 適用
                $("a.js-pjax").pjax("#main");
            });
        </script>
    </body>
</html>

これで上手くいくと思ったんですが、リンクをクリックしても #main の中身が非同期で書き換えられません。あれ?使い方間違ってる?ReadMe の通りに実装したんですけど…。


Firebugデバッグしてみたら、下記のエラーがコンソールに表示されていました。

options is not defined エラー
this.trigger('pjax.start' [xhr, options])

さらにデバッグを進めたところ、jquery.pjax.js の下記の場所で失敗。

pjax.defaults = {
  timeout: 650,
  push: true,
  replace: false,
  // We want the browser to maintain two separate internal caches: one for
  // pjax'd partial page loads and one for normal page loads. Without
  // adding this secret parameter, some browsers will often confuse the two.
  data: { _pjax: true },
  type: 'GET',
  dataType: 'html',
  beforeSend: function(xhr){
    this.trigger('start.pjax', [xhr, options]) // ← options が無い!
    xhr.setRequestHeader('X-PJAX', 'true')
  },
  error: function(){
    if ( textStatus !== 'abort' )
      window.location = options.url
  },
  complete: function(){
    this.trigger('end.pjax', [xhr, options]) // ← xhr と options が無い!
  }
}

この options はどこで定義されてるんですかね?ざっとみた感じでは見当たりません。ローカル変数ではいろんなところで定義されていますけど。


Pjax を呼び出している部分を下記のように修正したら回避できました。

$(function() {
    // Pjax 適用
    $("a.js-pjax").pjax("#main", {
        timeout: 3600,
        beforeSend: function(xhr) { // beforeSend を指定
           this.xhr_ = xhr; // xhr を保持しておく
           this.trigger('start.pjax', [xhr, this])
           xhr.setRequestHeader('X-PJAX', 'true');
       },
       complete: function() { // complete も指定
           // 保持しておいた xhr を使う
           this.trigger("end.pjax", [this.xhr_, this]);
       }
   });
});

めでたしめでたし。


それにしても、これってバグ?それとも使い方間違ってる?みんなすんなり使えてるみたいなので、私の環境だけなんでしょうかね…。