pjax第2回 「How to use pjax well?」

8109 0

連載モノとなっております。
以前のものをご覧になっていない方はそちらからご覧になると、より理解が深まると思います。

前回、pjaxにおける基礎知識を書かせていただきました。
今回は実装に関する実用的な内容を書かせていただきます。

5/9 内容を一部追記しました。

pjax初期設定

各プラグイン毎に使用方法が違っています。eventやoptionなどは各プラグインの公式ドキュメンテーションなどをご参照ください。

$(document).pjax('selector' , 'pjaxContainer-selector'/*, any options*/);
// or
$(document).on('click', 'selector', function(){
	// any processing
	$.pjax({
		container: 'pjaxContainer-selector'
		//any options
	});
});

// Setting Events
$(document).on('pjax:eventName',function(){
	// any processing
});
jquery-pjax
const Pjax = require('pjax-api').Pjax;
new Pjax({
	// link: 'selector'
	areas: [
		'some selector'
	]
});

// Setting Events
window.addEventListener('pjax:eventName', fun );
PJAX API

PJAXはデフォルトではすべてのaタグに対してイベントが紐付けられます。フィルタリングをさせたい際はオプションのlinkにクラスセレクタやtarget属性の有無などを指定すると良いでしょう。(※aタグ以外のタグでは動作しません。linkのhrefを取得する内部処理になっており、これを変更することはできません。)

また、PJAXのプラグイン(pjax-api.js)はES2015以降が使われているため、現状はpolifillを併用するほうが良いでしょう。

<script src="https://cdn.polyfill.io/v2/polyfill.js?flags=gated&#038;features=default,NodeList.prototype.@@iterator"></script>
Barba.Pjax.init();

// Setting Events
Barba.Dispatcher.on('eventName', fun );

Barba.jsでもデフォルトではaタグに対してのイベント紐付けが行われます。フィルタリングを行いたい際はBarba.Pjax.preventCheckのAPIを利用しましょう。

Barba.Pjax.preventCheck = (evt, element) => {
	if(!element) return;
	if(!element.classList.contains('pj')){
		return false; // リンクがpjクラスを持っていなければpjaxさせない
	}else{
		return true
	}
});

Barba.Pjax.preventCheckでtrueが返ればpjax、falseが返ればpjaxを行わないという指定になります。

html側の設定

基本として、optionとして指定したselectorがpjaxのエリアになります。(jquery-pjaxならcontainerに指定した要素、PJAXならareaに指定した要素。)
Barba.jsのみ、#barba-wrapper下の.barba-containerを切り替えるというエリア固定式です。(※id名、class名も固定ゆえこれらの付与必須)

<html>
<head>
</head>
<body>
	<div id="container">
		<header id="header">
			<h1>title</h1>
		</header>
		<!--pjaxの際はここから-->
		<main id="pjax-container" class="">
			<div id="inner" class="inner-content">
	
			</div>
		</main>
		<!--ここまでが入れ替わる(※ただし、見た目上は直下の子要素(#inner)から入れ替わったように見える)-->
		<aside id="sidebar">
			<p>sidebar</p>
		</aside>
		<footer id="footer">
			<p>copyright</p>
		</footer>
	</div>
<script></script>
</body>
</html>

jquery-pjaxとPJAXでの注意点ですが、ここでは#pjax-containerをpjaxで切り替えるエリアに指定したとします。
DOM上では#pjax-containerから置きかわりますが、遷移前に#pjax-containerに付与されていたクラスや属性値は継承される(JavaScript等で付与されたstyle属性などはそのまま残る)為、見た目上(感覚的に?)は#innerから入れ替わったかのような振る舞いをします。(=遷移先のクラスなどは反映されない)
しかし、DOM上は変更されている為、遷移前に#pjax-containerに付与されていたイベントなどは動作しなくなります。

// こうした指定のコードは遷移後動かなくなる
// -> pjax遷移後に同名の要素に対してイベント付与を行いたい場合、改めて同じ処理を行う必要がある
$('#pjax-container .foo').on('click', fn );
document.querySelector('#pjax-container .foo').addEventListener('click', fn );

// こういう指定のイベント(親ノードでのイベント定義)は遷移後でも動く
// =再付与する必要無し(というかすると付与した回数分処理が発生する)
$(document).on('click', '.foo', fn );
$('#container').on('click', '.foo', fn );

function check(e){
	let target = e.target;
	if(target.classList.contains('foo')){
		// some code
	}
}
document.getElementById('container').addEventListener('click', check );

pjaxで置き換わったエリアにはクラスやJavaScriptで動的に付与された属性値などは残る為、遷移後にそれらが邪魔になる場合は自前でリフレッシュする処理が必要
+#pjax-container内のイベントは再定義が必要

<main id="barba-wrapper" class="">
<!--pjaxの際はここから-->
	<div class="barba-container" data-namespace="namespace">

	</div>
<!--ここまでが入れ替わる-->
</main>

Barba.jsでは.barba-containerに対してidやdata-namespaceを指定できます。この属性を利用することで、ページ毎に違ったtransitionを利用したり、様々な処理を行ったりできます。

※必ずしもすべてのページ毎に一意のnemespaceを使用しなければならない訳ではありません。同じ処理を行いたいページ同士は同じnamespace名にすればOKです。

最後に、pjaxサイトを製作する際のサイト全体でのhtml設計として

このようにpjaxエリアの中にサイト全体で一意のidを付与することをおすすめします。(この要素でページ内のコンテンツをすべてwrapする)

こうすることでこの次の章にある「ページ毎のイベント処理」や、第1回目で述べた「pjax遷移後のGC(ガベージコレクション)」がシンプルになる為おすすめします。(必須ではない要素が一つ増えるのがあまり気持ち良くありませんが…)

Barba.jsでは#barba-wrapperとその子要素に.barba-containerを必須とする構造であり、.barba-containerにidを付与できる(+.barba-containerは完全に遷移先のものに置き換わる)という仕組みになっている為、上記の方法が非常にマッチします。それもあり一概に悪い手法とは言えないと思います。

ページ毎のイベント処理

Webサイトであればページ毎にまったく違った処理が必要になったりします。Blogでもアーカイブページと記事個別ページは処理を分けたい、なんてことも多いでしょう。

イベント分岐のさせ方は様々ありますが、すべてのプラグインで使える汎用例を紹介します。

  • urlから分岐
  • idの存在で分岐

個人的におすすめするのは、ページ判定用の関数と、各ページ毎のイベントをまとめた関数を作成しておき、pjaxの遷移後にコールバックさせる方法です。

ページ判定用関数例

urlから分岐

function pageCheck(){
	const url = location.href;
	if( /page1/.test(url) ){
		a();
	}else if( /page2/.test(url) ) ){
		b();
	}else{
		c();
	}
}

str.indexOf()を使ってもよいです。.match()ではなく.test()を使うのは処理速度で。

JavaScriptで文字列の有無判定方法の速度ベンチマーク(indexOf、match、test)

idの存在で分岐

function pageCheck(){
	if( document.getElementById('page1') ){
		a();
	}else if( document.getElementById('page2') ){
		b();
	}else{
		c();
	}
}

個人的にはこちらのidの存在によっての分岐をよく使います。
前回触れた、GCの為にpjaxエリア内に(サイト全体で)ユニークなidの要素を用意することがほとんどであることと、判定の速度も早いので。

ポイントとしては、jQueryを使用していてもこの際はNativeのdocument.getElementByIdを使用すること。
$('#id')でも遅くはないですが、jQueryの内部処理の関係上Nativeを使ったほうがさらに高速です。

ページ判定の処理には可能な限り時間がかからないようにするべきです。せっかく高速な遷移を実現しても、ここでもたついてしまっては本末転倒です

各ページ毎の処理用関数例

また、それぞれのページでのイベントはこのようにメソッドとしてまとめておくのもおすすめです。

const PageEvents = {
	page1: () => {
		// ページ1の時にのみ実行したい処理
	},
	page2: () => {
		// ページ2の時にのみ実行したい処理
	},
	elseFunc: () => {
		// それ以外の時にのみ実行したい処理
	}
	.
	.
	.
}

これに合わせてpageCheck関数もこのように変更します。

function pageCheck(){
	if( document.getElementById('page1') ){
		PageEvents.page1();
	}else if( document.getElementById('page2') ){
		PageEvents.page2();
	}else{
		PageEvents.elseFunc();
	}
}

各プラグインでの呼び出し方例

// jquery-pjax
$(document).on('pjax:end',function(){
	pageCheck();
});

// PJAX
window.addEventListener('pjax:load', pageCheck );

// Barba.js
Barba.Dispatcher.on('transitionCompleted', function(){
	pageCheck();
});

ここではpjax遷移後いちばん最後に呼ばれる(+popstate時にも呼び出される)イベントへコールバックさせましたが、これ以外のイベントでも問題ありません。(※上記のイベントで処理するのがいちばん安定しますが、それぞれのプラグインでこれより早いタイミングで処理を行えるイベントや方法があります)

pageCheckを関数化してあることで、window.load時にも同様にこの関数を呼び出すだけで、ページごとのイベントが呼び出されます。

// Use jQuery
$(function(){
	pageCheck();
	// some code
});

// Native
function init(){
	pageCheck();
	// some code
}
window.addEventListener('DOMContentLoaded', init, false);

このようにすると関数呼び出しのみで処理を分岐させられる為、同じ内容を何度も書かなくて済む、DRY(Don’t Repeat Yourself)なコードになりますね。

同様に、ページロード後の最初の一回だけに処理したい内容も下記のようにinit()関数内等で定義すると良いでしょう。

// Use jQuery
$(function(){
	pageCheck();
	// windowやdocumentなどの上位ノードに対して付与したいイベント定義
	// pjaxエリア外の要素に対して付与したいイベント定義など
});

// Native
function init(){
	pageCheck();
	// windowやdocumentなどの上位ノードに対して付与したいイベント定義
	// pjaxエリア外の要素に対して付与したいイベント定義など
}
window.addEventListener('DOMContentLoaded', init, false);

こうしたコーディングの利点として、ページロード時とpjax遷移後の処理が同じになる為、基本pjax後にイベントが付与されていないといったことが起こりません。また、「ページロード時の場合はこうでpjaxの時はこうで…」といったことを考えなくて済むようになり、混乱せずに済むようにもなります。

その他、全ページ共通で呼び出したいイベントもあるでしょう。(ex. lazyLoadやLightBox系プラグインなど)
そういった時にはPageEventsに遷移後必ず実行するメソッドを追加すると良いでしょう。

const PageEvents = {
	commonFunc: () => {
		if( document.querySelector('#contents .lazy') ){
			// 省略
		}
		if( document.querySelector('#contents .modal') ){
			// 省略
		}
	},
	page1: () => {
		// some code
	},
	page2: () => {
		// some code
	},
	elseFunc: () => {
		// some code
	}
	.
	.
	.
}

こちらもセレクタの存在判定で分岐させることができます。(他の方法でも全然問題ありません)

続いて、pageCheck関数の条件分岐外で該当メソッドを呼び出せばOKです。

function pageCheck(){
	if( document.getElementById('page1') ){
		PageEvents.page1();
	}else if( document.getElementById('page2') ){
		PageEvents.page2();
	}else{
		PageEvents.elseFunc();
	}
	PageEvents.commonFunc();
}

遷移時の演出

jquery-pjax

jquery-pjaxはリンクのクリックイベントに遷移前の演出、遷移後のアニメーションをpjaxイベントに紐付けます。

$(document).on('click', 'selector', function(e){
	e.preventDefault();
	let href = $(this).attr('href');
	// any processing

	$(pjaxContainer).fadeOut( 400, () => { // ←遷移前のアニメーション
		$.pjax({
			url: href,
			container: pjaxContainer
			//any options
		});
	});
});

$(document).on('pjax:end',function(){
	$(pjaxContainer).fadeIn( 400, () => {
		// 遷移後のアニメーション
	});
});

一応下記のようなシンプルな実装も可能です。色々処理を挟みたい場合は前者を使用してください。

$(document).pjax('selector' , 'pjaxContainer-selector');

$(document).on('pjax:send', function() {
 	// 遷移前のアニメーション
});
$(document).on('pjax:complete', function() {
	// 遷移後のアニメーション
});

PJAX

PJAXでは遷移時をpjax:fetchイベントへ、遷移後をcontent update後のイベントへ紐付けると良いでしょう。
function inAnimation(){
	// 遷移前のアニメーション
}
function outAnimation(){
	// 遷移後のアニメーション
}

window.addEventListener('pjax:fetch', inAnimation );
window.addEventListener('pjax:load', outAnimation );

Barba.js

Barba.jsではBarba.BaseTransition.extend()にアニメーション用のメソッドを作成し、Barba.Pjax.getTransitionに紐付けます。

const animation = Barba.BaseTransition.extend({
	start: function() {
		Promise
			.all([this.newContainerLoading, this.goOut() ])
			.then(this.finish.bind(this))
	},
	goOut: function(){
		// 遷移前のアニメーション
	},
	finish: function(){
		// 遷移後のアニメーション
	}
});
// You can set other animations

Barba.Pjax.getTransition = function() {
	const transitionObj = animation;
	return transitionObj;
};

animationはもっとシンプルな実装にしても全然問題ありません。

また、これらのコードから分かる通り、特に指定をしなければjquery-pjax以外はリンククリックでの遷移、popstateでの遷移に関わらず演出が行われます。

popstate(戻る/進む)時の処理方法

ちょくちょく見かけるのですが、前述の方法をとっていれば特に処理を分ける必要はありません。

もし、何らかの理由でpopstate時とpjax遷移時とで処理を分けたい場合は、リンクをクリックしたかどうか?で判別する方法をとるのが良いでしょう。
※PJAXにはクリックイベントに関するAPIがない為popstateのみ処理を分ける、という実装は諦めてください。

let popstate = false;
 
$(document).on('pjax:popstate', function(){
    popstate = true;
    // popstate時の処理
    popstate = false; // 注:pjax:endが呼び出される前にこの処理が行われないように、非同期で処理するべき
}).on('pjax:end',function(){
    if( popstate ) return;
    // 通常遷移時の処理
});
let linkClicked = false;

Barba.Dispatcher.on('linkClicked', function(HTMLElement){
	linkClick = true;
});
Barba.Dispatcher.on('transitionCompleted', function(currentStatus,prevStatus){
	if(!linkClicked){
		// popstate時の処理
		linkClicked = false;
	}else{
		// 通常遷移時の処理
	}
});

こんな感じで処理を挟んでやるとpopstate時と通常遷移で処理を分岐させることもできます。(※あくまでも例なので他にやり方があると思います。)

ただし、ブラウザの戻る/進むを使用する際は「早く戻りたい」というケースがほとんどであること、そしてモバイルブラウザのフリックによるpopstate時ではモバイルブラウザ固有のエフェクトが存在するため、アニメーションは邪魔になりやすいという点は忘れないでください。
僕は基本popstate時のアニメーションは省きます。(※jquery-pjax使用時。Barba.jsではそのままにしていることが多いですが)

クリック時のカスタマイズ

jquery-pjaxとBarba.jsではクリック時にイベントを挟むことができる為、そこで条件分岐を挟めばクリックしたリンクによって処理を変えることができます。

いくつか例を挙げると

  • 特定のクラス、属性を持っている場合pjaxさせない
  • リンクによって遷移時のアニメーションを変える
  • リンクによって遷移させるエリアを変更する(※jquery-pjaxのみ)

これらを応用すると

このように、ページ毎に違ったアニメーションや、クリックしたリンクによってアニメーションを変更することができます。

このBlogのこのトピックス(Web Resource)だけでも3パターンの遷移アニメーションを利用しています。(※ナビのリンクとこの記事のprev、Nextのリンクで違ったアニメーションになります+記事個別かアーカイブページかで同じリンクでも違うアニメーションになるリンクも)

それぞれのプラグインによって実装方法は結構違っているため、若干内容がごちゃついてきましたね…。
次回からはそれぞれのプラグイン一つずつに絞った内容でお送りしようと思います。

次回は「pjaxのイベント処理」についてもっと突っ込んだ内容を書いていきます。
その次にjquery-pjaxに関する内容を、その次にBarba.jsに関する内容、という順で書いてまいります。(PJAXはここに書いた以外はほとんど共通の内容だけなので割愛…)

最後に

今回紹介したコードは、pjaxを使用した製作の雛形として使えるかと思います。せっかくなのでここに一つにまとめたものを置いておきます。

const PageEvents = {
	commonFunc: () => {
		if( $('#contents .lazy').length ){
			// 省略
		}
		if( $('#contents .modal').length ){
			// 省略
		}
	},
	page1: () => {
		// ページ1の時にのみ実行したい処理
	},
	page2: () => {
		// ページ2の時にのみ実行したい処理
	},
	elseFunc: () => {
		// それ以外の時にのみ実行したい処理
	}

}
function init(){
	// windowやdocumentなどの上位ノードに対して付与したいイベント定義
	// pjaxエリア外の要素に対して付与したいイベント定義など

	$(document).on('click', 'selector', function(e){
		e.preventDefault();
		let href = $(this).attr('href');
		// any processing

		$(pjaxContainer).fadeOut( 400, () => { // ←遷移前のアニメーション
			$.pjax({
				url: href,
				container: pjaxContainer
				//any options
			});
		});
	});

	$(document).on('pjax:end',function(){
		pageCheck();
	});
}

function pageCheck(){
	if( document.getElementById('page1') ){
		PageEvents.page1();
	}else if( document.getElementById('page2') ){
		PageEvents.page2();
	}else{
		PageEvents.elseFunc();
	}
	PageEvents.commonFunc();
}

$(function(){
	pageCheck();
	init();
});
const PageEvents = {
	commonFunc: () => {
		if( document.querySelector('#contents .lazy') ){
			// 省略
		}
		if( document.querySelector('#contents .modal') ){
			// 省略
		}
	},
	page1: () => {
		// ページ1の時にのみ実行したい処理
	},
	page2: () => {
		// ページ2の時にのみ実行したい処理
	},
	elseFunc: () => {
		// それ以外の時にのみ実行したい処理
	}
	.
	.
	.
}
function init(){
	pageCheck();
	// windowやdocumentなどの上位ノードに対して付与したいイベント定義
	// pjaxエリア外の要素に対して付与したいイベント定義など

}

function pageCheck(){
	if( document.getElementById('page1') ){
		PageEvents.page1();
	}else if( document.getElementById('page2') ){
		PageEvents.page2();
	}else{
		PageEvents.elseFunc();
	}
	PageEvents.commonFunc();
}

window.addEventListener('DOMContentLoaded', init, false);

質問やご指摘などはコメントやtwitterでも受け付けておりますので、お気軽にお願いします。

pjaxplugin

Comments

Add a Comment

メールアドレスが公開されることはありません。

認証コード * Time limit is exhausted. Please reload CAPTCHA.

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください