pjax こそが pushState + Ajax の本命

pjaxの前にpushStateとは

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

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

有名なところではGitHubで使われてるアレ。

hash fragment (/#!/)

ブラウザの履歴を機能させるため、URL の fragment (#) を使ってAjaxなページを実現する方法。一時期もてはやされた感があるが、さらなる「#!」URL批判 - karasuyamatenguの日記 など合理的な反論があり、これから導入するのはためらわれるところ。

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

pjaxとは

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

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

「高機能なWebブラウザでは見栄えよく、そうでないWebブラウザで“も”それなりに」
Webページの見栄えにどこまでこだわるのか | 日経 xTECH(クロステック)

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

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

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

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

使い方

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

細かい使い方はいろいろあるようなので、GitHubにあるREADMEを参照すると良い。GitHub - defunkt/jquery-pjax: pushState + ajax = 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を実現できるので流行るといいな。