吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

History APIのpushState / popState

2015年11月2日 / category : その他-lab

前回の記事で解説したHistory APIを使用したサンプルは、プラグイン「Kazitori.js」を使用したサンプルでした。

■History APIを使用したコンテンツの入れ替え
http://tsudoi.org/weblog/?p=4148

今回は、このプラグインを使用せずに、History APIのpushState/popStateを使用し、ページ遷移をシームレスに切り替えるサンプルを作りました。

http://tsudoi.org/html5/history-api/
※長崎ページのみ、写真をクリックすることができます。

このコンテンツの仕組みについて解説していきます。
仕組みは大きく分けて2つあります。

1. History APIを使用して履歴を保存する
→ History APIのpushState/popStateによる実装

2. ページ遷移(コンテンツ切り替え)をシームレスに
→ htmlファイルを先読みし、画像をロードした後にコンテンツを切り替えることによってシームレスを実装

それではこの仕組みについて解説していきます。

1. History APIを使用して履歴を保存する

履歴を保存するためには、History APIのpushStateメソッドを使用します。
このメソッドの解説の前に、そもそもHistory APIとはなんでしょうか。
Historyオブジェクトは昔からありましたので、次のようなコードで参照することができます。

Historyオブジェクトの参照
window.history

昔からよく使われていたのは、ボタンをクリックしたら前のページに戻る場合などでしょうか。

Historyオブジェクトを使用して「戻る」
<a href="#" onclick="history.back()">

このHistoryオブジェクトに、HTML5から新たに追加された機能に、pushStateメソッド、popStateイベントがあります。
pushStateメソッドは、履歴を保存する際に使用します。

HistoryオブジェクトのpushStateメソッド
history.pushState(null,null,'/html5/history-api/');

history.pushState(stateプロパティ, タイトル, 履歴に保存するURL);

pushStateメソッドには、引数が3つあります。
第1引数には、イベントオブジェクトのstateプロパティ。
第2引数には、タイトル。
第3引数には、履歴に保存するURL。
となります。
今回のサンプルでは、第1引数、第2引数にはnullを入れています。

続いて、popStateイベントですが、ブラウザの「戻る」「進む」が実行されたタイミングでこのイベントが発生します。
使い方は他のイベントと同じですが、次のような感じです。

HistoryオブジェクトのpopStateイベント
$(window).on('popstate', function(_e){
	//処理内容
});

2. ページ遷移(コンテンツ切り替え)をシームレスに

この仕組みは、前回の記事で作成したサンプルと同じですが、改めてここで詳しく解説します。
流れは、次のような感じです。

1.ローディング開始
2.切り替えるコンテンツのURLを読み込む
3.コンテンツに使用する画像参照先をまとめたjsonファイルを読み込む
4.ローディング停止
5.コンテンツを切り替える

それではこの流れを、先ほどのHistory APIのコードも含め、全体の流れをコードで解説します。
フレームワークは、jQueryを使用します。基本的に、設定する関数は、jQueryの関数・メソッドとして継承させて記述していきます。
必要なjQueryの関数は、

loadHTML関数 ・・・ htmlファイルをロードする
openLoading関数 ・・・ ローディング要素をオープン
closeLoading関数 ・・・ ローディング要素をクローズ
replaceTitle関数 ・・・ タイトル要素を置き換える
replaceContents関数 ・・・ コンテンツ要素を置き換える
openContents関数 ・・・ コンテンツ要素をオープン
closeContents関数 ・・・ コンテンツ要素をクローズ
getLoadqueue関数 ・・・ 画像をローディングする

以上、8つです。
必要なjQueryのメソッドは、

setButtonメソッド ・・・ リンク設定

以上、1つです。
サンプルはTypeScriptで実装していますが、上記の継承させる関数・メソッドは、次のようにinterfaceに定義しました。

関数・メソッドをinterfaceに定義
interface JQueryStatic{
	loadHTML: any;
	openLoading: any;
	closeLoading: any;
	replaceTitle: any;
	replaceContents: any;
	openContents: any;
	closeContents: any;
	getLoadqueue: any;
}

interface JQuery{
	setButton: any;
}

続いて、実際に継承させる記述は下記のような感じです。

関数・メソッドをjQueryに継承
!function(_win, _j){
	_j.extend({
		loadHTML: function(){},
		openLoading: function(){},
		closeLoading: function(){},
		replaceTitle: function(){},
		replaceContents: function(){},
		openContents: function(){},
		closeContents: function(){},
		getLoadqueue: function(){}
	});
	_j.fn.extend({
		setButton: function(){}
	});
}(window, jQuery);

続いて、windowオブジェクトにも必要な変数を追加しておきます。これもinterfaceに定義しました。

変数をinterfaceに定義
interface Window{
  root: string;
  body: any;
  loading: any;
  wrapper: any;
  ajax: any;
  pageHtmlObj: any;
  pageHref: any;
};

root ・・・ コンテンツのルートになるディレクトリを設定
body ・・・ body要素のjQueryオブジェクト
loading ・・・ ローディング要素のjQueryオブジェクト
wrapper ・・・ wrapper要素のjQueryオブジェクト
ajax ・・・ $.ajax()の返り値を設定
pageHtmlObj ・・・ 切り替えるコンテンツのhtmlコードを一時的に保存するjQueryオブジェクト
pageHref ・・・ 切り替えるコンテンツのURL

続いて、全ページ共通のhtmlファイルの構成ですが、次のようになっています。

全ページ共通のhtmlファイルの構成
<div class="container"> //container要素
<div id="wrapper"> //wrapper要素
<div id="contents"> //contents要素
<script> this.json = 'xxxx.json'; </script>
<script src="xxxx.js"></script>

</div>
</div>
<div id="loading"></div> //loading要素
</div>

wrapper要素は、コンテンツを切り替える際に、フェードイン・アウトをかける要素になります。
contents要素は、JS側でコンテンツを切り替えるコード部分になります。
loading要素は、ローディングです。
xxxx.jsonは、そのコンテンツで使用する画像参照先をまとめたファイルとします。
実際の構成は、次のような感じです。
http://tsudoi.org/html5/history-api/json/index.json

xxxx.jsは、そのコンテンツで実行するJSを記述します。

それでは、JSのコードを見ていきます。
※コードは、TypeScriptです

まず、jsファイルの構成ですが、次の2ファイルに分けています。
plugins.js ・・・ プラグインをまとめたjsファイル
base.js ・・・ サイト全体を制御するjsファイル

plugins.jsには、次のプラグインを圧縮して記述しています。
jQuery v2.1.4
PreloadJS

それでは、サイト全体を制御しているbase.jsを見ていきます。
解説する際に使用するコードは、全てTypeScriptになります。

base.js – 初期設定
$(function(){
	window.root = '/html5/history-api/';
	window.body = $('body');
	window.loading = $('#loading');
	window.wrapper = $('#wrapper');
	$('.btn').setButton();
	if( window.history && window.history.pushState ){
		$(window).on('popstate', function(_e){
			_e.preventDefault();
			window.pageHref = location.pathname.replace(window.root,'');
			$.closeContents();
		});
	};
	window.pageHref = location.pathname.replace(window.root,'');
	$.openLoading();
});

class=”btn”となっているリンク要素に、setButtonメソッドを実行します。
setButtonメソッドについては、後述します。

先ほど解説したpopStateイベントを設定します。ユーザーが「戻る」「進む」を実行した際に、
現在のディレクトリをpageHrefプロパティに格納し、closeContents関数を実行します。
closeContents関数については、後述します。

コンテンツにアクセスすると、まずopenLoading関数が実行されます。
openLoading関数は次ようにしました。

base.js – openLoading関数
openLoading: function(){
	_win.loading.css({visibility: 'visible'}).animate({'opacity': 1}, 300, function(){
		_j.loadHTML( _win.pageHref );
	});
}

loading要素をフェードインさせ、その後loadHTML関数を実行します。
引数には、切り替えるコンテンツのURLが格納されています。
この時、画面はローディング画面となっています。

base.js – loadHTML関数
loadHTML: function(_href){
	_win.pageHtmlObj = _j('<div>');
	if( _win.ajax ) _win.ajax.abort();
	var _def = _j.Deferred();
	_win.ajax = _j.ajax({
		type: 'GET',
		dataType: 'html',
		url: (_win.root+_href)
	}).done(function(_data){
		_win.pageHtmlObj.empty().html(_data);
		_j.replaceTitle( _win.pageHtmlObj.find('title') );
		_j.replaceContents( _win.pageHtmlObj.find('#contents') );
		_def.resolve();
	}).fail(function(){
		console.error('error');
		_def.reject();
	});
}

pageHtmlObj変数に、空のdiv要素のjQueryオブジェクトを格納します。
ajaxプロパティに、オブジェクトがあった場合は、abortメソッドを実行しておきます。
これは不要になった通信をキャンセルするためです。
続いて、ajax関数でhtmlファイルを読み込みます。
読み込み後、そのhtmlファイルのコードを、pageHtmlObj変数に格納してあるjQueryオブジェクトのhtmlに代入し、replaceTitle関数、replaceContents関数を実行します。
replaceTitle関数の引数には、タイトル要素を設定し、replaceContents関数の引数には、contents要素を設定しています。
この時、画面はまだローディング画面となっています。

それではreplaceTitle関数を見ていきます。

base.js – replaceTitle関数
replaceTitle: function(_title){
	_j('title').text( _j(_title).text() );
}

引数_titleには、読み込んだhtmlファイルのtitle要素が格納されています。
このtitle要素のテキストを、現在のtitle要素のテキストに挿入します。
これでタイトルが切り替わったことになります。

続いて、replaceContents関数を見ていきます。

base.js – replaceContents関数
replaceContents: function(_contents){
	_j('#contents').empty().replaceWith( _contents ).ready(function(){
		_j.loadImages();
	});
}

引数_contentsには、読み込んだhtmlファイルのcontents要素が格納されています。
jQueryのreplaceWithメソッドを使用して、要素を入れ替えています。
これでcontents要素が切り替わったことになります。
ただし、画面はローディング画面のままなので、表示上はわかりません。
要素が切り替わり、DOMの準備ができたタイミングでloadImages関数を実行します。

それでは、loadImages関数を見ていきます。

base.js – loadImages関数
loadImages: function(){
	var _def = _j.Deferred();
	_j.when(
		_j.getJSON(_win.json)
	).done(function(_data){
		var _loadqueue = _j.getLoadqueue();
		_loadqueue.on('complete', function(_e){
			_j.closeLoading();
		});
		_loadqueue.loadManifest(_data);
	}).fail(function(){
		console.error('error');
		_def.reject();
	});
}

jQueryのwhen関数を使用しています。これは非同期処理を行う際に使用します。
まず、jQueryのgetJSON関数で、jsonファイルを読み込みます。
jsonファイルの参照先が、_win.jsonになっていますが、これは先ほど読み込んだcontents要素の中に、

<script>
this.json = ‘/html5/history-api/json/index.json’;
</script>

という記述を入れておくことで参照することができます。
このjsonファイルには、そのコンテンツで使用する画像の参照先をまとめておきます。

実際の構成は、次のような感じです。
http://tsudoi.org/html5/history-api/json/index.json

jsonファイルを問題なく読み込めたタイミングで、jQueryのdone関数が実行されます。
done関数の処理では、まず_loadqueue変数に、getLoadqueue関数の返り値を格納しています。

getLoadqueue関数を見ていきます。

base.js – getLoadqueue関数
getLoadqueue: function(){
	var _createjs = _win.createjs;
	var _loadQueue = new _createjs.LoadQueue;
	_loadQueue.setMaxConnections(3);
	return _loadQueue;
}

この関数では、createjsを使用します。
createjsは、plugins.jsに記述しておきます。createjsのLoadQueueクラスを生成し、returnします。

それでは、loadImages関数に戻ります。
LoadQueueクラスのloadManifestメソッドで、画像の読み込みを始めます。引数には、読み込んだjsonファイルを格納します。
jsonファイルに記述してある画像参照先の画像を全て読み込んだタイミングで、completeイベントが発生します。
completeイベントでは、closeLoading関数が実行されています。

それでは、closeLoading関数を見ていきます。

base.js – closeLoading関数
closeLoading: function(){
	_win.loading.animate({'opacity': 0}, 300, function(){
		_j.openContents();
		_j(this).css({visibility: 'hidden'});
	});
}

ローディング要素をフェードアウトさせています。フェードアウトが終わったタイミングで、openContents関数を実行しています。

それでは、openContents関数を見ていきます。

base.js – openContents関数
openContents: function(){
	_win.wrapper.animate({'opacity': 1}, 300, function(){
		_win.body.trigger('complete');
	});
}

wrapper要素をフェードインさせています。
closeLoading関数とopenContents関数の実行を画面で見ると、ローディングがフェードアウトして、コンテンツがフェードインする画面になります。
フェードインが終わったタイミングで、body要素のcompleteイベントを発生させています。
このcompleteイベントは、読み込んだcontents要素の中に別のjsファイルを読み込み、そのjsの中に次のような記述をしておくことで、実行されることになります。

contents要素の中に別のjsファイル
!function(_win, _j){
	if( _win.body ) _win.body.one('complete', function(){
		//個別のページ毎の処理
	});
}(window, jQuery);

これで一連の流れが完了となります。
続いて、メニューボタンをクリックしたときの動作を見てみます。
該当するボタン要素にclass=”btn”を設定しておきます。

初期設定の中で、

$(‘.btn’).setButton();

を実行しています。

それでは、setButtonメソッドを見ていきます。

contents要素の中に別のjsファイル
setButton: function(){
  _j(this).each(function (){
    var _this = _j(this);
    _this.off('click').on({
      'click': function(e){
        e.preventDefault();
        if( _win.pageHref !== _this.attr('href') ){
          _win.pageHref = _this.attr('href');
          _j.closeContents();
          history.pushState(null,null,_win.root+_win.pageHref);
        };
        return false;
      }
    });
  });
}

if文では、同じページではなかったら・・・という分岐をしています。
同じページでクリックしても処理は走りません。
if文の中の処理を見ていきます。
まず、ボタン要素のhref属性をpageHref変数に格納します。
この変数に格納されたURLが次に切り替わるコンテンツとなります。
続いて、closeContents関数を実行しますが、これ以降の流れは解説済みですので、割愛します。
最後に、History APIのpushStateメソッドで、pageHref変数に格納されたURLを履歴に保存します。

以上となります。