第4回 「jquery-pjax」

547 0

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

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

jquery-pjaxの特徴

jquery-pjaxは、jQueryを使っているだけあって、シンプルで直感的なコードで様々な処理を行えるのが魅力です。また、クリックイベントのカスタマイズが非常に行いやすく、aタグ以外の要素や他のイベントからでもpjaxを行えます。

提供されている$(document).pjax(selector, [container], options)といったインターフェース以外に、もっとローレベルでの処理を可能とする$.pjax.click$.pjaxといった関数が用意されています。

//
$(document).pjax(selector, [container], options)

$.pjax( options ); // これをクリックイベントへ紐付ける

$(document).on('pjax:eventName',function(){
	// any processing
});

この為、クリックするターゲット毎に様々な処理を行うことが可能になっています。

イベント処理が簡単

jQueryを使えるということは、こういったコード構成だったとして

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
	<div id="container">
		<header id="header">
			
		</header>

		<!-- pjax Area -->
		<main id="pj-container">
			
		</main>
		<!-- pjax Area&#91;End&#93; -->

		<aside id="sidebar">
			
		</aside>

		<footer id="footer">
			
		</footer>
	</div>
</body>
</html>
$(function(){
	$('#container').on('click', '.pj', function(){
		let href = $(this).attr('href');
		$.pjax({
			url: href,
			container: '#pj-container',
			fragment: '#pj-container'
		});
	});
});

例えば、といったコードだけですべてのpjaxのクリックイベントを定義することができます。
pjaxエリアの上のnodeでのイベント定義なので、pjax遷移後でも問題なく機能するため、読み込み時最初の一度の定義で再定義を必要としない方法です。

aタグ以外の要素でもpjaxが可能

さらに、クリックするターゲット毎に様々な処理を行えるということは、極端な話、aタグ以外でもpjaxを行わせることもできます。(※これはjquery-pjaxのみ可能です。)

<ul id="sub-nav">
	<li class="pj-li cat-item"><a href="">カテゴリー1</a><span class="post-count">(12)</span></li>
	<li class="pj-li cat-item"><a href="">カテゴリー2</a><span class="post-count">(8)</span></li>
	<li class="pj-li cat-item"><a href="">カテゴリー3</a><span class="post-count">(10)</span></li>
</ul>

例えばこのようになっているリストリンクがあったとします。(wordpressのメニューを使うとこんな感じでaタグの外にテキストが出力される)こういったhtml構造で、それでもリンクをliのサイズ一杯にしたい…なんてことがあるかもしれません。
そういった時は

$('#sub-nav').on('click', 'pj-li', function(){
	let href = $(this).find('a').attr('href');
	$.pjax({
		url: href,
		container: '#pj-container'
	});
});

このように、liをターゲットに、クリックした時、その子孫にあるaタグからhref属性値を取得して…といったようにすることもできます。

pjaxイベントが豊富

Documentationを見れば全て載っていますが、豊富なイベントが用意されています。

// For example
$(document)
	.on('pjax:send', fn1) // ajaxリクエスト時の処理
	.on('pjax:complete', fn2) // pjax遷移直後に発火する処理(※注:popstate時も発火)
	.on('pjax:popstate', fn3) // popstate時のみの処理
	.on('pjax:error', fn4); // ajax通信がエラー時の処理

一部のみでも様々なタイミングでイベント処理を行うことができます。また、上記のようにメソッドチェーンで繋いで記述することができ、それらの処理をひとまとめにしたコードインターフェースを作成することも容易です。

jQueryベースの豊富で簡単なイベント管理が使える

使用のためにjQueryを必要とすることもあり、イベントに直感的に名前空間を利用できたり、.off()が使えるため、イベントの削除も簡単に行えます。

$('selector').on('click.pj', fn ); // 特定の要素に対してpjaxのクリックイベントを付与
$(document).on('click.pj-common', '.pj', fn) // html内に存在する.pjを持つ要素すべてにpjaxのクリックイベントを付与

$('selector').off('.pj'); // 特定の要素のpjaxクリックイベントを削除
$(document).off('.pj-common') // 名前空間"pj-common"に紐付けたクリックイベントを削除

pjaxの「実は」

以前の回でこのような図を用いて

「差分の箇所のみ置き換える」と言っていましたが、実はXHRで読み込まれているのはページの「全データ」です

htmlへパースされるのがエリアの箇所のみというだけであって、読み込むのは丸々のページです。(といってテキストデータ状態なのでせいぜい数Kバイト程度の違いですが…※これは他のpjaxプラグインも同様)

その為、それも無駄と感じる際は、jquery-pjaxではリクエストヘッダーに_pjaxが付与されていることを利用して pjax時とそうでない時で返すデータを変えるという処理を加えると良いでしょう。

<?php
	$pjax = filter_input( INPUT_GET, '_pjax' );
	if( !$pjax ){
		// 非pjax時(Ex. headerなどpjaxエリア外の要素をこのif分岐内へ)
?>
<!DOCTYPE html>
<html lang="ja">
	<!-- 省略 -->
<?php
	} // if(!$pjax):&#91;End&#93;
?>
	<main id="pjax-container" class="foo bar" data-title="This page title">

		<!-- 省略 -->

	</main>
<?php
	if( !$pjax ){ // 非pjax時(Ex. footerなどpjaxエリア外の要素をこのif分岐内へ)
?>
	<footer id="footer">

	</footer>
	<script></script>
</body>
</html>
<?php
	} if(!$pjax):&#91;End&#93;
?>

このようにすることでpjaxで読み込んでくるhtmlを完全にpjaxエリアのみにすることができます。 この実装を行うだけでも(ケースによっては)数百ミリ秒読み込み速度が上がったりします

また、この際pjaxのエリア(container)に指定した要素のdata-title属性値から遷移後のタイトルを書き換える仕様になっている為、この属性を指定していないと自前で実装する必要があります。(タイトル出力用の関数を用意しておくと良いでしょう。)

ただし、pjaxエリア内にインラインSVGがあり、かつそのSVGにtitle属性が含まれている場合、titleがそのSVGのtitleに置き換わります。
※読み込んだデータにtitle属性が含まれる場合、その中の最後のtitleを取得する仕組みになっている為です。=>SVGのtitle属性を省くことでtitleの汚染は防ぐことができます。(アクセシビリティは少し下がりますが…)

また、wordpress使用時に上記の指定を行った際、popstate時にバグが発生する場合があります。(historyが正しく反映されないなど。こちらは原因がいまいちよく分かりません…)

これらの場合は読み込み速度が犠牲になりますが、上記のif分岐での実装は行わないほうが良いかもしれません。(※全データを読み込む形式にするとバグが発生しなくなる)

応用編

さて、ここまでのことを理解したなら、このプラグインを“使いこなして”いきましょう。

jquery-pjaxでは『クリックイベントに対して色々と処理を挟める』と言いました。
つまるところ

$('#container').on('click', '.pj', function(e){
	e.preventDefault();

	if( $(this).hasClass('foo') ){
		let href = $(this).attr('href');
		let options = {
			url: href,
			container: '#sub-container',
			fragment: '#sub-container',
		};
	}else if( $(this).hasClass('bar') ){
		let href = $(this).find('a').attr('href');
		let options = {
			url: href,
			container: '#page',
			fragment: '#page',
		};
	}else{
		let href = $(this).attr('href');
		let options = {
			url: href,
			container: '#pj-container',
			fragment: '#pj-container',
		};
	}

	$.pjax( options );
});

といったように、クリックした要素によって、pjaxの設定を切り替えることもできます。
これにより、pjaxのエリアを変更したり、クリックした要素によって遷移アニメーションを切り替える
といったことも可能です。

注意:pjaxのエリア変更はバグの温床となりやすいため(特にpopstate時)、しっかりデバッグを行うことと、ある程度使用に慣れてから使うことをおすすめします。

また、このブログのように、prevとnextでアニメーションの左右を反転させる際なども
$('#pj-container').on('click', '.pj', function(){
	if( $(this).hasClass('.prev') ){
		let direction = ['-100px','100px'];
	}else{
		let direction = ['100px','-100px'];
	}

	$('#container').animate({opacity: 0,left: direction[0]}, 200, function(){
		$('#pj-container').css({left: direction[1]});
		$.pjax( options );
	});
});

のような感じで行えばシンプルに済ませられます。

この他にも、こうした一時変数を用いた方法で分岐させる方法もあります。
使い方としては、状態を判断するためのもので遷移完了後に状態をリセットするといった使い方をします。

let flag = 0;

function anime1(option){
	$('#sub-container').slideUp( 800, function(){
		$.pjax( option );
		flag = 1;
	});
}
function anime2(option){
	$('#page').animate({opacity:0, top: '100px'}, 800, function(){
		$.pjax( option );
		flag = 2;
	});
}
function anime3(option){
	$('#pj-container').animate({opacity:0}, 1200, function(){
		$.pjax( option );
		flag = 0;
	});
}

$(function(){
	$('#container').on('click', '.pj', function(e){
		e.preventDefault();

		if( $(this).hasClass('foo') ){
			let href = $(this).attr('href');
			let options = {
				url: href,
				container: '#sub-container',
				fragment: '#sub-container',
			};
			anime1();
		}else if( $(this).hasClass('bar') ){
			let href = $(this).find('a').attr('href');
			let options = {
				url: href,
				container: '#page',
				fragment: '#page',
			};
			anime2();
		}else{
			let href = $(this).attr('href');
			let options = {
				url: href,
				container: '#pj-container',
				fragment: '#pj-container',
			};
			anime3();
		}
	});

	$(document).on('pjax:end', function(){
		if( flag === 1 ){
			$('#sub-container').slideDown(800, function(){
				flag = 0;
			});
		}else if( flag === 2 ){
			$('#page').animate({opacity:1, top: 0}, function(){
				flag = 0;
			});
		}else{
			$('#pj-container').animate({opacity:1}, 1200, function(){
				flag = 0;
			});
		}
	});
});

どのアニメーションが呼ばれたかによって、遷移完了後のpjaxエリアのアニメーション(表示方法)やエリアを切り替えるという方式です。
アニメーションの種類によってflag変数へ状態を格納し、遷移完了後に変数の値をリセットします。

この方法であれば、状態はアニメーション中のみしか状態を保持しないため、バグは起こりにくいでしょう。(セレクタ付与による状態管理も同様で、遷移完了後にリセットするといった使い方がベター)

変数やセレクタを用いた状態管理は、popstate時に基本狂います。なぜなら、変数の状態や付与されたセレクタは(変更処理を実装していなければ)pjax後も変更されないためです。
こと、popstate時は前のページが何であるかが一定しません。そのため、「このページならこのアクションを」をといった分岐処理を別に用意しなければ意図した通りの処理が行われることはありません。(例えば)URLによる分岐などの処理が同時に必要となってくる、ということを忘れないようにしてください。

もし、pageCheck関数をid判定にしていた場合、URL判定の別の関数が必要になったり、pageCheck関数内にまた処理を追加しなければならなくなるため、コードがスパゲティ化しやすくなります。実装前に設計をしっかりと行いましょう。

ページ毎のクリックイベント制御

冒頭では一括での制御方法を書きましたが、当然ケースによっては各ページ毎に分けたいこともあると思います。

function pjElements(container,element){
	$(container).on('click', element, function(e){
		e.preventDefault();
		// 省略...
	})
}

こういったイベント付与用の関数を用意しておいて、pageCheck時にセレクタを渡すようにしておくとDRYなコードになるでしょう。

function pjElements(container,element){
	$(container).on('click', element, function(e){
		e.preventDefault();
		// 省略...
	})
}

const PageEvents = {
	commonFunc: () => {
		if( document.querySelector('#contents .lazy') ){
			$('#pj-container .lazy').lazyLoad(); // pjaxエリアのid+クラスセレクタで定義する
		}
		if( document.querySelector('#contents .modal') ){
			// 省略
		}
	},
	page1: () => {
		pjElements('#pj-container', '.sub-pj');
		// some code
	},
	page2: () => {
		pjElements('#pj-container', '.sub-pj');
		// some code		
	},
	elseFunc: () => {
		pjElements('#pj-container', '.othr-pj');
		// some code
	}
}

$(function(){
	pageCheck();


	$('#container').on('click', '.pj', function(e){
		e.preventDefault();
		let href = $(this).attr('href');
		// 省略...
	});

	$(document).on('pjax:end', function(){
		// 省略...
	})
});

#pj-containerなどpjaxエリアのidや、その直下の子要素でイベントを定義するとpjax遷移後にこれらの要素は置き換わり、付与したイベントも自動でjqueryが削除してくれるため、おすすめです。

また、ここでの.pjのように、常に存在するクリックイベントに紐付いたセレクタがある場合は、それ以外の名称のセレクタを利用しましょう。(※同じ要素にそれぞれを同時に付与しないこと。イベント重複します)

windowやdocumentのイベントの処理

特定のページでのみscrollやresizeのイベントを用いたい時があると思います。そういった時は

function scrollEv(){
	const far1 = $(window).outerHeight();
	const far2 = far1 * 2;

	$(window).on('scroll', function(){
		let move = $(window).scrollTop();

		if( move > far1 ){
			// some code
		}else if( move > far2 ){
			// some code
		}
	})
}

function pjElements(container,element,casees){
	$(container).on('click', element, function(e){
		e.preventDefault();

		// 省略...

		// 引数caseseに渡した値によって特定のイベントを削除(※遷移前にイベントを削除する)
		if( casese === 1 ){
			$(window).off('scroll'); // 関数で分けてもOK
		}else if( casese === 2 ){
			// 省略...
		}

		$.pjax( option )
	})
}

const PageEvents = {
	page1: () => {
		pjElements('#pj-container', '.sub-pj', 2);
		// some code
	},
	page2: () => {
		scrollEv();

		pjElements('#pj-container', '.sub-pj', 1);
		// some code		
	},
	elseFunc: () => {
		// some code
	}
}

といった感じで処理すると良いでしょう。

jQueryの.offは対象が存在しなかった場合でもエラーが出なかったりしますが、それに甘えた処理はしないようにしましょう。

まとめ

jquery-pjaxでは

  • クリック時に処理を挟むことができる
  • aタグ以外からでもpjaxを行うことができる
  • クリックする要素によってイベント、遷移させるエリアを切り替えることができる
  • jQueryのサポートにより、イベントの管理がNativeに比べ遥かに容易

これらより、比較的複雑な処理を組み込むことが可能です。こうした特徴をもとに『こんなこともできるんじゃないかな?』ということを色々と試していってみてください。
きっと、まだ見ぬ素晴らしい演出やUXが生まれると思います。

他にも気になったところや「こういうところをもっと知りたい」などありましたら、コメント等でお知らせいただけると嬉しく思います。

pluginpjax

Comments

Add a Comment

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

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.