Sumally PJAX

奥山幸彦 / @FiNGAHOLiC

PJAXとは?

pushState + ajax

pushState

HTML5から導入されたURLを操作するAPIの一つで、ブラウザの履歴を追加するためのメソッド

ajax

サーバーからページに必要なデータを非同期で取得し、インターフェイスの構築を行う技術の総称

PJAX使用前

https://sumally.com

https://sumally.com/p/1

HTML

JS

CSS

IMG

HTML

JS

CSS

IMG

PJAX使用後

https://sumally.com

ajaxでデータのみリクエスト

HTML

JS

CSS

IMG

HTML

pushStateで任意のURLに変更

実装コード


// リンクがクリックされたらURLを変更
$('.js-pjaxLink').on('click', function (e) {
    e.preventDefault();
    var url = $(this).attr('href');
    window.history.pushState('', '', url);
});

// コンテンツをレンダリング
var render = function (html) {
    $('.js-pjaxContents').html(html);
};

// コンテンツをサーバーから非同期で取得
var loadContents = function (url) {
    $.ajax({ url: url }).done(function (res) {
        render(res.html);
    });
};

// URLの変更を検知
$(window).on('popstate', function () {
    loadContents(window.location.pathname);
});

Thanks!

そう簡単にはいかない

じゃあ何が問題か?


// リンクがクリックされたらURLを変更
$('.js-pjaxLink').on('click', function (e) {
    e.preventDefault();
    var url = $(this).attr('href');
    window.history.pushState('', '', url);
});

現在閲覧しているページのリンクを押下してしまうと、pushStateして履歴を追加しているのでブラウザの戻るボタンで戻ると同じページを延々と見ることになる。

かといってリンクをクリックさせないのもおかしい。

問題点


// リンクがクリックされたらURLを変更
$('.js-pjaxLink').on('click', function (e) {
    e.preventDefault();
    var url = $(this).attr('href');
    var method = 'pushState';
    if (isSamePath(url)) {
        method = 'replaceState';
    };
    window.history[method]('', '', url);
});

現在閲覧しているページのリンクが押下された場合は、replaceStateを使用して現在の履歴を置き換える

※実際はハッシュ(#tutorial)やクエリ(?object_id=11111111)も処理しないとダメなんでもっと複雑になる…。

解決策


// URLの変更を検知
$(window).on('popstate', function () {
    loadContents(window.location.pathname);
});

ブラウザによってpopStateの挙動が違う。

Safariではonload時にpopstateイベントを発行するが、

その他ブラウザ(Chrome、FF、IE)はpopstateイベントを発行しない。

※昨年実装した時点では、Chromeはonload時にはpopstateイベントを発行しないが、

外部サイトに移動してからブラウザの戻るボタンで戻ってくるとpopstateが発行されていた。

問題点


// URLの変更を検知
$(window).on('popstate', function () {
    if (isInitialLoad && isSafari) {
        isInitialLoad = false;
        return;
    }
    loadContents(window.location.pathname);
});

Safariで初回閲覧時はpopstateイベントを無視するようにする。

e.originalEvent.stateでpushStateしたかどうかを調べて、pushStateされていなければ実行しないという判定方法もあるが、ブラウザの戻るボタン対策で再度サーバーからデータを取得、レンダリングするコストを削るため、キャッシュとして使用する目的でonload時にページの情報をreplaceStateで格納済みなので使用できない。

解決策

Sumally内を遷移後、外部サイトへ移動。その後ブラウザの戻るボタンで戻ると、必ずDOMContentLoadedを実行して欲しいのに、実行されたりされなかったりと不安定な挙動だった。また、HTMLが最新のものではなく、閲覧時のキャッシュを使用しているようだった

問題点


header("Cache-Control:no-cache,no-store");

解決策

Back-Forwad Cache、通称bfcacheページの戻る、進むボタン時にページを高速に描画するためにページの状態をキャッシュして復元させるブラウザの機能)が原因で起こっていた。bodyタグにonunloadを指定する、metaタグでno-cacheを指定する等、古い?情報がネットに転がってるが、現状では上記コードで全てのブラウザでbfcacheを回避出来る。

スクロール位置を復帰させるためにpopstate発行時に現在の画面のスクロール位置を保存し、ブラウザの戻るボタンで戻ってきた時に保存していた値を使用してスクロール位置を復帰させたかったが、Firefoxではpopstate発行時に画面のスクロール値を取得しようとすると正しく取得できなかった。

問題点

コストは高いが、スクロールする毎に閲覧ページのスクロール位置を保存しておき、popstate時にその値を閲覧ページのスクロール位置として保存する。

また、SafariのスワイプによるNext/Backだとpopstate発行時から500msほど遅延させてからでないとスクロール位置が復帰できなかった。

pinterestもその方式を採用していた。

解決策

Thanks!

Sumally PJAX

By Okuyama Yukihiko

Sumally PJAX

  • 439