Pjaxでスムーズなページ遷移

シームレスなページ遷移が可能になる falsandtru版 の Pjax の説明です。

はじめに

※このページは falsandtru版 Pjax-API について説明しています。 defunkt版 の記事はこちら。

falsandtru の最新版では jQuery プラグインではなくなりました。お手軽にはじめたい場合は Ver2.x を使用するか上記の defunkt版 をおすすめします。(最新版に合わせて書き直し中 2017年1月現在)

Pjaxとは

Pjax とは Ajax で非同期にコンテンツを入れ替えるのと同時に URL も変更してくれるテクニックです。ページ遷移のたびに全てをレンダリングせずに、更新が必要な部分だけをレンダリングするので素早いレスポンスが可能でエフェクトを付ける事も出来ます。

falsandtru版はモダンブラウザ のみのサポートになり IE (Edgeを除く)では動きませんが、高機能& defunkt版 Pjax の問題点が色々解決されているのでオススメです(•ө•)ノ  Electron 等の開発でも活躍しそうです。

Ver3 では ルーター機能がついたそうなのでサーバーサイドに API を作成すれば SPA のようなサイトも作れるのでは?と思ったのですが、まだ詳しく試していないのでおいおい調べて書いていこうと思います。

PHP で作られた素敵な CMS「MODX」だと pjax も非常に簡単に実装できます。

デモ

サイトを全面Pjaxにしたためデモページは別途作り直し中です。

MODX を使ったサーバーサイド処理をして Pjax リクエスト時に本文、ページタイトル+ヘッダー内容を更新させています。デベロッパーツールにあるネットワークを見るとリクエスト数の違いがわかると思います。

インストール

Ver3.x

コマンドラインから npm でインストールします。Node.js は予めインストールされていると想定します。

プロジェクトフォルダに移動し以下のコマンドを実行します。

$ npm i pjax-api spica

pjax-api spica の2つのパッケージがインストールできます。

node_modules の中のインストールされた2つのフォルダの中にそれぞれ dist というフォルダがあるので、その中にある「spica.js」「pjax-api.js」または min ファイルを適当なフォルダにコピーして使用します。

コンパイル済みの JavaScript ファイルが含まれるパッケージを見つけられなかったのですが、過去の issues を読むと作者の意向なのかもしれません。(もしあれば教えてください…)

http://falsandtru.github.io/pjax-api/ 

Ver2.x

Ver2.x は jQuery プラグインとして動作します。このバージョンはリリースパッケージに dist ファイルが含まれているのでそのまま使用できます。

jQuery本体と、jquery.pjax.jsをダウンロードしてください。

Pjaxの動作の図解

Ver3.x Pjax-API の使い方

Pjaxのセットアップ

var Pjax = require('pjax-api').Pjax;
new Pjax({
areas: [
  '#primary' // 入れ替えるエレメント
]
});

デフォルトはターゲット指定のない全てのリンクのレスポンスから Pjax エリアを取得できるかを確認します。areas の2つめと3つめでレスポンスに対象エレメントがない場合にチェックする外側のエレメントを指定できます。

特定のリンクのみ対象にする場合は link オプションでセレクタを指定できます。以下はクラス名に .pjax が付いたターゲット指定のないリンクのみ Pjax 処理します。

new Pjax({
link: 'a.pjax([target])',
areas: [
  '#primary',
]
});

Pjax に対応したHTMLを書く

Pjax-API では全体から必要な部分だけを検索するので基本的にサーバー側で処理をしなくても使用できます。レスポンスに Pjax で指定したエレメントが存在すればそこだけを更新します。

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>page1</title>
    <script src="/js/spica.js"></script>
    <script src="/js/localsocket.js"></script>
    <script src="/js/pjax-api.js"></script>
    <script>
        var Pjax = require('pjax-api').Pjax;
        new Pjax({
            areas: [
                // 更新するエレメント
                '#header, #primary',
                // 無かった場合に更新するエレメント
                '#container'
            ]
        });
    </script>
</head>
<body>
    <div id="container">
        <h1>Pjaxテスト</h1>
        <div id="header">
            <h2>ここはページ1</h2></div>
        <div id="primary">ページ1の中身</div>
    </div>
</body>
</html>

サーバー側で振り分け処理を行う

サーバー側で処理を行い Pjax に必要なレスポンスのみを返す事もできます。 PHP を使った場合の遷移は以下のような動作をします。

PHPで振り分ける場合

Pjax リクエスト時ヘッダーに X-Pjax = 1 が付加されるのでそれで判定します。

$header = getallheaders();
if (array_key_exists('X-Pjax', $header)) {
// Pjax リクエストで吐くHTML
} else {
// それ以外の直アクセス等で吐くHTML
}

PHP で Pjax リクエスト時に吐くHTML

デフォルト設定で HTML ヘッダーの title, base, meta, link タグを更新してくれるので、振り分け処理をする場合は対象エレメント以外にこのタグも必ず含める必要があります。例えばツイッターカード等の内容もきちんと更新されます。外部 CSS の URL も入れておきましょう。

<head>
<meta ... >
<title>タイトル</title>
<link ... >
</head>
<body>
<div id="header">ヘッダー</div>
<div id="primary">ページの内容</div>
</body>

レスポンスに必要なエレメントがあることを信用できる場合は不要な処理を飛ばして必要最低限のエレメントのみ返せば良いです。ただしフォールバックエリアを指定している時にリクエスト先のページ構成が異なるような場合、一部の表示コンテンツが無い…という自体が起きるので注意が必要です。

非対応ブラウザへの配慮

IEでは全てのバージョンでエラーが起きるので例えば以下のように IE 以外で判定してスクリプトを読み込むようにするといいかもしれません。気にしなくてもいいかもですが…。$.getScript や $.ajax だと初回に通常遷移してしまったので DOM にタグを追加するようにしてます。

// IEを判定
var ua = navigator.userAgent.toLowerCase();
var isIE = ua.indexOf('msie') > -1 || ua.indexOf('trident/7') > -1;
// IEでなければ読み込み&実行
if (!isIE) {
    var javaScriptLoader = function(src) {
        return function(){
            var d = $.Deferred();
            var script = document.createElement('script');
            script.addEventListener('load', function() {
                return d.resolve();
            }, false);
            script.src = src;
            document.body.appendChild(script);
            return d.promise();
        };
    }
    javaScriptLoader('/js/spica.min.js')()
    .then(javaScriptLoader('/js/localsocket.min.js'))
    .then(javaScriptLoader('/js/pjax-api.min.js'))
    .then(function(){
        var Pjax = require('pjax-api').Pjax;
        new Pjax({
            areas: [
                '#container'
            ]
        });
    });
}

アクセス解析などの対策

Google アナリティクスに関しては以下に詳しく書いてあります。

単一ページ アプリケーションのトラッキング

 window.ga が無ければアクセス解析を開始します。

if (!window.ga) {
// Google Analytics のコード
}

 pjax:ready のイベントを使ってページが遷移したことを知らせます。

document.addEventListener('pjax:ready', function() {
if (window.ga) ga('send', 'pageview', window.location.pathname);
}, false)

Googleアドセンス広告の場合は <script src="//~"> の外部スクリプトの読み込み箇所だけ遷移外のヘッダーフッター等に移動させて、他の部分はタグで囲んでその範囲も遷移させるか、または  pjax:ready で ins タグの再描写& ...push のコードを実行させます。

Ver2.x jquery.pjax の使い方

Pjaxのセットアップ

パラメータのareaで入れ替わる対象を設定して、特定のリンクに絞る場合はlinkを設定します。以下の例は target 属性がない全てのリンクを対象にしてクリックすると#pjaxContentが入れ替わります。

以下はページの内容、見出し、パンくずを更新させます。

$.pjax({
// 置き換えるコンテナのID カンマで区切って複数可能
area : '#pjaxContent, #pageTitle, #breadcrumbs',
 // target属性が無いリンクを指定
link : 'a:not([target])'
});

クラス名をつけたりセレクタを指定する事で特定のエリアのリンクに絞り込む事もが出来ます。

以下の例は.mainの中の.pjaxというクラス名が付いたリンクだけを対象にします。

// セレクタを指定する場合はドキュメントを読み込んだあとに。
$(document).ready(function() {
$('.main').pjax({
area : '#pjaxContent',
link : 'a.pjax' // pjaxを行うリンクを限定(ない場合全てのリンクが対象)
});
});

サーバーサイドアプリ側の設定

Pjax からヘッダー(またはパラメータ)付きの Ajax リクエストが発生するので、 PHP などを使って「ヘッダーの X-Pjax が true の場合はコンテナ部分だけを出力する」という条件で動的に HTML 出力するようにします。PHP のgetallheaders()関数が使用できない場合は$_GET['pjax']または$_POST['pjax']で判定する事ができます。

<?php
$header = getallheaders();
if ($header['X-Pjax']) {
  // pjaxリクエストの場合の処理&コンテナ内のHTMLだけ出力
} else {
  // pjaxリクエストじゃない場合の処理&全てのHTMLを出力
}
?>

非Pjaxリクエスト時に吐くHTML

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>タイトル</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.pjax.js"></script>
<script type="text/javascript">
$.pjax({
  area : area : '#pjaxContent, #pageTitle, #breadcrumbs',
  link : 'a.pjax'
});
</script>
</head>
<body>
<h1 id="pageTitle">ページタイトル</h1>
<div id="breadcrumbs">パンくずナビゲーション</div>
<ul class="links">
<li><a href="page1.html" class="pjax">page1</a></li> <li><a href="page2.html" class="pjax">page2</a></li> <li><a href="page3.html" class="pjax">page3</a></li>
</ul> <div id="pjaxContent"> ページの内容 </div> </body> </html>

Pjaxリクエスト時に吐くHTML

<title>タイトル</title>
<h1 id="pageTitle">ページタイトル</h1>
<div id="breadcrumbs">パンくずナビゲーション</div>
<div id="pjaxContent"> ページの内容 </div>

こんなかんじで転送量をだいぶ減らせるのと、アプリ側のロジックや設定にもよりますがエリア範囲外でメニューやページリストなどを動的に吐いている場合、それらの処理をスルーできるのでサーバー側のコストも減らせそうです。

静的サイト、またはサーバー処理をしないで実装する

falsandtru版 の Pjax ライブラリはデフォルトでレスポンスのデータからエリアを確認するので、エリア外のコンテンツも含まれていた場合必要部分だけを抽出します。そのためサーバーサイドで分岐の処理をしなくても Pjax を実装できます。また動的サイトでもアプリ側で特に処理をしなくても手軽に実装ができます。

サーバーサイドのスクリプトを使って必要な部分だけ受け取るのと違って、全て受け取ってから必要な部分だけを抜き出すため転送量的には通常の遷移と変わりませんが体感速度は割りと早くなります。

Pjaxに対応するHTMLを書く

遷移させたいコンテンツを適当なID、以下は「pjaxContent」というIDで囲みます。
Pjax に対応している場合この中だけが遷移先のIDの中身と入れ替わるので、Pjax 元と Pjax 先のページではこのIDのコンテナが存在するようにしておきます。

$.pjax({
area : '#pjaxContent', // 置き換えるコンテナのID
link : 'a.pjax'  // pjaxを行うリンクを限定(ない場合全てのリンクが対象)
});

遷移元(発火点)「page1」のHTML

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>page1</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.pjax.js"></script>
<script type="text/javascript">
$.pjax({
  area : '#pjaxContent',
  link : 'a.pjax:not([target])'
});
</script>
</head>
<body>
<h1>pjaxテスト</h1>
<ul class="links"> <li><a href="page2.html" class="pjax">page2</a></li> <li><a href="page3.html" class="pjax">page3</a></li>
</ul> <div id="pjaxContent"> <h2>ここはページ1</h2> <div class="main">ページ1の中身</div> </div> </body> </html>

遷移先「page2」のHTML

<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>page1</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.pjax.js"></script>
</head>
<body>
<h1>pjaxテスト</h1>
<div id="pjaxContent">
  <h2>ここはページ2</h2>
  <div class="main">ページ2の中身</div>
</div>
</body>
</html>

処理が早いとちゃんと遷移しているのか分かりにくいですが、page2 でリロードするとリンクがなくなる事でコンテナだけ遷移したことが確認できると思います。

相互移動させる場合には page2 にも$.pjax({...})の記述が必要になるので、実用するには外部ファイル化して読み込ませるのがいいと思います。

切り替えエフェクトの付け方

callbackcallbacksの任意のプロパティにエフェクト処理をする関数を設定します。

callbacks.beforeでOUTエフェクト、callbackでINエフェクトを実行します。
ちなみに、callbackは更新処理が終わったあとに実行されるので対象エリア内のエレメントはすでに無くなっている状態です。エリア内のエレメントにアクセスしたいならタイミングはupdate.content.beforeあたりでもいいかなーと思います。色々試し中(●'◡'●)

以下は OUT で左にスライドしながらフェードアウトして IN で右からフェードインするエフェクトをつけます。

$.pjax({
  area : '#pjaxContent',
  link : 'a.pjax:not([target])',
  callback : function() {
    $('#pjaxContent').css({left:20}).animate({
      left : 0,
      opacity : 1
    }, 100); // エフェクトの時間
  },
  callbacks : {
    before : function() {
      $('#pjaxContent').animate({
        left : -20,
        opacity : 0
      }, 100);
    }
  },
  ajax: { timeout: 3000 }, // 読み込みにこれ以上かかる場合は通常遷移に移行 wait : 120 // エフェクト分待ち時間を作る });

OUT エフェクトがある場合切り替わり途中に内容が変わってしまったりするのでwaitパラメータで最低限の待ち時間を設定します。この数字はミリ秒なので 1000 で 1秒 です。あまり長すぎるエフェクトは逆にユーザビリティが悪化するので短めにするといいと思います。

ローディングエフェクト

同じくcallbacks.beforeでエフェクトを開始して、callbackでエフェクトを終了させます。

$.pjax({
  area : '#pjaxContent',
  link : 'a.pjax:not([target])',
  callback : function() {
    $('div.loading').fadeOut(500);
  },
  callbacks : {
    before : function() {
      $('div.loading').fadeIn(100);
    }
  },
ajax: { timeout: 3000 }, wait : 100 });

でも、大抵の場合ローディングエフェクトいらないぐらい早いんじゃないでしょうか…。

ページ読み込み時の処理

プロパティのcallbacks.updateで JavaScript や CSS の再読み込みを設定できますが、簡単なものは Pjax イベントで実行できます。

以下の例はページ読み込み時にリンクタグにlinkというクラスを付けます。

$(document).on('ready pjax.ready', function() {
  $(".content").find("a").addClass('link');
});

パラメータとイベント

パラメータはすべてパラメータ用オブジェクトのプロパティに設定して渡します。

プロパティと登録できるイベントの一部です。イベントはかなり多いので自由度が高いです。全部引用してもしょうがないので基本的なもの&使用頻度の高そうなものをピックアップしました。(私もまだ試していないものもあるので後でちょっとだけ増やします)全てを見る場合は配布元の Readme を見て下さい!説明文は Readme から引用させて頂きました。

エフェクトなどを付ける場合はcallbackcallbacksに関数を設定します。

プロパティ

callbacks(イベント処理の登録)

$.pjax({
  callbacks : {
    update : {
      complete : function(event, arg) { console.log(arg + ': update.complete'); }
    }
  }
});