Mach3.laBlog

“Pjax” – Alphabetical Advent Calendar 2013

この記事は賞味期限切れです。(更新から1年が経過しています)

“P” は Pjax の “P”。

P

Pjax

Pjax とは、pushState + Ajax から作られた言葉で、 History の pushState メソッドでURLを管理しつつ、 コンテンツの遷移を Ajax を使って行う手法です。

おおまかな流れはこのような感じ。

  1. ページ内のリンクをクリックした時にページ遷移を行わずに、 対象のHTMLファイルの内容を Ajax で取得します。
  2. 取得に成功したら History.pushState でURLを変更して履歴に追加します。
  3. ファイルの内容から必要なコンテンツ部分を抽出して、コンテンツのコンテナに流し込みます。

ページごと遷移しない為余計な部分の再描画が必要なく、 遷移時に好みのエフェクトをかけられる点などが特徴です。 また、サイト全体に共通にかかるような JavaScript などの処理をページ移動毎に行わなくて済む為、 高速化にも一役かっています。

その実用性についてはこちらにまとまっていましたので、ご参考までに。

Pjax コンテンツを習作してみる

実装の仕方は様々あると思いますが、ここで一例習作してみましょう。 (例をシンプルにする為にjQueryを使用します)

HTML構造を共通化する

<ul id="navi">
    <li><a href="/" data-pjax>Home</a></li>
    <li><a href="/foo.html" data-pjax>Foo</a></li>
    <li><a href="/bar.html" data-pjax>Bar</a></li>
    <li><a href="/baz.html" data-pjax>Baz</a></li>
</ul>

<div id="content">
    <!-- ここの内容を差し替えます -->
    <h1>Hello, Pjax</h1>
</div>

まずこのようなHTMLを想定します。 上のナビゲーション部分のリンクをクリックすると Ajax でリンク先のHTMLファイルを取得し、 #content の内容を差し替えます。

HTMLの構造は、遷移元・遷移先共に共通にしておきましょう。 例えば下のコードは Foo をクリックした時に表示する為の foo.html です。

<!-- foo.html -->
<head>
    <title>Page Foo</title>
</head>
...
<ul id="navi">...</ul>

<div id="content">
    <h1>Foo</h1>
    <p>Lorem ipsum ...</p>
</div>

pushState のサポートをチェックする

if(! history.pushState || ! history.replaceState){
    return;
}

この例では pushState / replaceState 共に実装されていなければ正常に動かないので、 サポートしていない環境では以降の処理をスキップし、そのままネイティブの機能でページを移動してもらいます。

リンクの制御

クリックの制御は イベントのバブリング を利用して document にて行う事にします。 これならば、コンテンツ部分に新たに生成されるリンクも制御できます。 Pjax で遷移するリンクを明示する為に、ここでは data-pjax 属性を使用しています。

$(document).on("click", "[data-pjax]", function(e){
    e.preventDefault();
    var href = e.currentTarget.href;
    $.get(href, function(html, status, xhr){
        // コンテンツを更新する
        updateContent(html);
        // 履歴に追加する
        history.pushState({url: href, html: html}, "", href);
    }, "html");
});

コンテンツの更新

上のコードで使用している updateContent() 内でHTML文字列から必要な部分を抽出してコンテンツを更新します。 今回必要とするのは #content の中身と title です。

var updateContent = function(html){
    var title = (function(){
        var m = html.match(/<title.*?>([\s\S]+?)<\/title>/i);
        return m ? m[1] : "";
    }());
    var content = (function(){
        var m = html.match(/<body.*?>([\s\S]+?)<\/body>/i);
        if(m){
            var content = $("<div>").append(m[1]).find("#content");
            return content.length ? content.html() : "";
        }
        return "";
    }());
    document.title = title;
    $("#content").hide().html(content).fadeIn();
};

content のパースは厄介なので、jQueryで一時的に要素を生成してフィルタリングしています。 遷移時にはコンテンツ部分がフェードインするようにしてみました。

popstate イベントの設定

これだけではリンクをクリックした時だけしかコンテンツが変わらないので、 ブラウザの進む・戻るボタン等で履歴を移動した場合にもコンテンツが切り替わるように、 popstate イベントの設定をします。

$(window).on("popstate", function(e){
    var state = e.originalEvent.state;
    if(state && state.html){
        updateContent(state.html);
    }
});

jQuery.fn.on でイベント設定した場合は e.state ではなく e.originalEvent.state と辿らなければいけないので注意が必要です。 pushState で登録しておいた html が state から取得出来るので、それを使用してコンテンツをその時の状態に戻します。

現在いるページで初期化をする

一番最初に訪れたページの内容で、state の初期化を行います。 そうしなければ、ページ遷移を行ったあとに一番はじめのページまで履歴をさかのぼった時に state が空っぽな為、コンテンツを更新してくれません。

history.replaceState({url: location.href, html: $("html").html()}, "", location.href);

ファイナルコード

シンプルな構成ですが、こんな感じになりました。 実際にはエラー時の処理や、コンテンツのキャッシュ等も入るかもしれません。

(function(){

    if(! history.pushState || ! history.replaceState){
        return;
    }

    var updateContent = function(html){
        var title = (function(){
            var m = html.match(/<title.*?>([\s\S]+?)<\/title>/i);
            return m ? m[1] : "";
        }());
        var content = (function(){
            var m = html.match(/<body.*?>([\s\S]+?)<\/body>/i);
            if(m){
                var content = $("<div>").append(m[1]).find("#content");
                return content.length ? content.html() : "";
            }
            return "";
        }());
        document.title = title;
        $("#content").hide().html(content).fadeIn();
    };

    $(document).on("click", "[data-pjax]", function(e){
        e.preventDefault();
        var href = e.currentTarget.href;
        $.get(href, function(html, status, xhr){
            updateContent(html);
            history.pushState({url: href, html: html}, "", href);
        }, "html");
    });

    $(window).on("popstate", function(e){
        var state = e.originalEvent.state;
        if(state && state.html){
            updateContent(state.html);
        }
    });

    history.replaceState({url: location.href, html: $("html").html()}, "", location.href);

}());

ライブラリを利用する

上の説明用のコードは単機能で汎用性にも欠け、いささか面倒です。 既に Pjax のライブラリは多くの方が公開していると思いますので、 それを利用させてもらいましょう。

jquery-pjax

https://github.com/defunkt/jquery-pjax

jquery-pjax は名前の通り Pjax の面倒な処理ひと通りこなしてくれるjQueryプラグインです。

$(document).pjax("a[data-pjax]", "#container");

jquery-pjax は Ajaxのリクエストに X-PJAX というヘッダを追加してくれます。 サーバサイドでそのヘッダを検知して、共通レイアウトを省いたコンテンツ部分のみを出力するようにすれば、 データ量の節約になるという寸法です。

レイアウトを省かないHTMLを読み込んで部分だけ使用したい場合(上であげたコードのようなケース)は、 fragment オプションでセレクタを指定する必要がありますので注意しましょう。

$(document).pjax("a[data-pjax]", "#container", {
    fragment: "#container"
});

参考資料

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*