pjaxの前にpushStateとは

AjaxとjQueryの説明は不要として、pushStateとはなんぞや。

pushStateを使ってブラウザの履歴に対する操作をし、HTMLの一部のみを書き換える動作でもブラウザの戻る/進む機能を実現できる方法のひとつ。Ajaxなページを再現し、かつURLを見慣れた方法で自然にpermalinkを表現できる。有名なところではGitHubで使われてるアレ。

hash fragment (/#!/)

ブラウザの履歴を機能させるため、URL の fragment (#) を使ってAjaxなページを実現する方法。一時期もてはやされた感があるが、 http://d.hatena.ne.jp/karasuyamatengu/20110212/1297465199 など合理的な反論があり、これから導入するのはためらわれるところ。

有名なところではTwitterで使われているあの厄介者。

pjaxとは

pjax とは pushState + ajax を合わせた語で、その名のとおり pushState を使いつつ Ajax な処理を行う為の jQuery ライブラリ。 GitHub の defunkt が開発していることもあり、今後 pushState を使ったものでは導入が進んでいく可能性がかなり高いのではないかと読んでいる。

また、最近ではページの見た目の面では非クロスブラウザを許容する風潮があるように見受けられ、この流れともpjaxは親和性が高い。

「高機能なWebブラウザでは見栄えよく、そうでないWebブラウザで“も”それなりに」

http://itpro.nikkeibp.co.jp/article/Watcher/20110329/358885/

pjax の振る舞いは、 Chrome のようなブラウザでは pushState を使い、 IE のようなブラウザでは通常のアクセスと同じように、全く同じ permalink でアクセスできるようにしてくれる。

「コンテンツがcurlでロードできなければそのサイトは壊れている勢力」の救世主

私もこの勢力のうちのひとりだと内心思っているのでやや傾倒している感はある。

しかし hash fragment を使用したページは curl では取得できない。これはサーバ側に fragment 以降は送信されないためであるが、 public なものに curl や LWP::UserAgent や Google のクローラーといったクライアントでアクセスさせるために URL に細工することは解せない。かと言って自分でゴリゴリと pushState の実装を書くのも骨が折れる。そういった問題を解決してくれるものになると思われる。

使い方

ここではざっくりとした使い方を書いておく。

細かい使い方はいろいろあるようなので、GitHubにあるREADMEを参照すると良い。https://github.com/defunkt/jquery-pjax

クライアントサイド

"js-pjax" クラスのアンカーに対してのみ機能させる場合はこのようにセレクタを書く。

<script src="/jquery.min.js"></script>
<script src="/jquery.pjax.js"></script>
<script type="text/javascript">
  $(function () {
    $('a.js-pjax').pjax('#main');
  })
</script>
サーバサイド

pjax を使ったリクエストの場合に、 HTTP ヘッダに X-PJAX: true が付くのでサーバサイドではヘッダを見て返すbodyを変更する。

もし、 X-PJAX がない場合はすべてをレンダリングした HTML を返し、X-PJAX が true の場合には対象のコンテナにロードさせるのに適切な response body を返すよう処理を書く。X-PJAX のリクエストの場合でタイトルを変更したい場合は、<title>タグも含めて応答する。

実装

サーバをPerlで実装したので

$ cd /tmp
$ curl https://gist.github.com/raw/901139/c13279a29cfcca8cc75e63fb9eeb65b3ca2785c7/app.psgi -LO 
$ plackup

で起動し、リクエスト/レスポンスをチェックできる。

app.psgi
use strict;
use warnings;
use feature qw/say switch/;
use Data::Section::Simple;
use Text::Xslate;
use Plack::Request;

my $tx = Text::Xslate->new(
    path => [ Data::Section::Simple->new->get_data_section ],
);

my $app = sub {
    my $req = Plack::Request->new(shift);
    my %data = ( %ENV,
        TIME => scalar localtime,
        PJAX => ($req->header('X-PJAX') ? 1 : 0)
    );

    say "----- X-PJAX is " . ($data{PJAX} ? 'TRUE' : 'FALSE');
    my $type =  $data{PJAX} ? 'pjax' : 'default';

    my $res = $req->new_response(200);
    $res->content_type('text/html; charset=utf-8');

    given ($req->path_info) {
        when ('/') {
            $data{title} = "root";
            $res->body( $tx->render("root-$type.tx", {data => \%data}) );
        }
        when ('/home') {
            $data{title} = "/home";
            $res->body( $tx->render("home-$type.tx", {data => \%data}) );
        }
        when ('/help') {
            $data{title} = "/help";
            $res->body( $tx->render("help-$type.tx", {data => \%data}) );
        }
        when ('/favicon.ico') {
            $res->redirect("http://www.google.com/favicon.ico", 301);
        }

        default {
            $res->status(404);
            $res->body('Not Found');
        }
    }

    return $res->finalize;
};

$app;

__DATA__

@@ home-pjax.tx
    <: if $data.PJAX { :> <title><: $data.title :></title> <: } :>
    <p> Hello, <: $data.USER :> </p>
@@ home-default.tx
    : cascade base;
    : override main -> { include "home-pjax.tx" }
    : override title -> { "/home" }

@@ help-pjax.tx
    <: if $data.PJAX { :> <title><: $data.title :></title> <: } :>
    <pre> <: $data | dump :> </pre>
@@ help-default.tx
    : cascade base;
    : override main -> { include "help-pjax.tx" }
    : override title -> { "/help" }

@@ root-pjax.tx
    <: if $data.PJAX { :> <title><: $data.title :></title> <: } :>
    <p> pjax!! pjax!! pjax!!</p>
@@ root-default.tx
    : cascade base;
    : override main -> { include "root-pjax.tx" }
    : override title -> { "root" }

@@ base.tx
    <!DOCTYPE html>
    <html>
        <head>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
            <script src="http://pjax.heroku.com/jquery.pjax.js"></script>
            <meta charset='utf-8'> 
            <title> <: block title -> { :> hello pjax <: } :> </title>
            <script type="text/javascript">
                $(function () {
                    $('a.js-pjax').pjax('#main');
                })
            </script>
        </head>
        <body>
            : include "nav.tx"
            <div>
            : $data.TIME
            </div>
            <div id="main">
                : block main -> { }
            </div>
        </body>
    </html>

@@ nav.tx
    <ul id="nav">
        <li><a href="/" class="js-pjax">Index</a></li>
        <li><a href="/home" class="js-pjax">Home</a></li>
        <li><a href="/help" class="js-pjax">Help</a></li>
    </ul>
サンプルについて

pjax リクエストを飛ばす方法、レスポンスをどう返せば意図したとおりに動くか、レスポンスを受けてタイトルを変更する方法など、手っ取り早く知りたいであろう事柄を埋めこんであるので、多少は参考になると思う。

curl で gist から取ってくる方法が一番楽だと思う。

結論

簡単に pushState による履歴操作と hash fragment (裏側のURLは汚い)に勝る綺麗な URL を実現できるので流行るといいな。