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

History APIを使用したコンテンツの入れ替え

2015年10月21日 / category : その他-lab

ある案件で「pushStateを使用して、ページ遷移をシームレスに」という要望がありました。
この手法は、最近のwebサイトで見かけるようになりました。
では、どんな仕組みになっているのでしょうか。

まずは、デモページを作ってみたので、どんな画面遷移になるか確認してみてください。

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

メニューをクリックすると、通常のページ遷移ではなく、ローディング後に画面が切り替わるような画面遷移になっていると思います。
さらに、URLも変更されており、ブラウザの「戻る」「進む」にも対応しています。

この仕組みのポイントとなるものがHistory APIです。これはHTML5の機能のひとつです。
History APIを使用することで、ページの内容を動的に変更することができるようになりました。

今回は、History APIを素敵な感じに操作できるプラグイン「Kazitori.js」を使用しました。

Kazitori.js
http://kazitori.org/
※素敵なプラグインです。

Kazitori.jsの詳しい使い方は、上記のURLより確認してみてください。

では、ページの構成はどうなっているか見てみます。

ページ構成
<!DOCTYPE html>
<html lang="ja">
<head>
<title>タイトル</title>
</head>
<body>
<header>ヘッダー要素</header>
<nav>グローバルナビ要素</nav>
<div>コンテンツ要素</div>
<div>ローディング要素</div>
<footer>フッター要素</footer>
</body>
</html>

全ページ(全html)上記のような構成となっており、赤字になっている要素がコンテンツが入れ替わる要素です。
コンテンツが切り替わる流れは次のようなイメージです。

1.メニューをクリック
2.ローディング開始(コンテンツに使用する画像の先読み)
3.クリック先のURLの内容をHistory APIを使用して取得(タイトル要素、コンテンツ要素)
4.現在表示されているコンテンツを非表示にする
5.ローディング完了
6.取得した要素を入れ替える & URLを変更 & ブラウザの履歴に登録
7.ローディング停止
8.入れ替わったコンテンツを表示する

1~7の流れは、jQueryで制御しています。
赤字になっている「3」「6」に関しては、History APIを使用する仕組みのメインとなる工程です。

まず、必要なプラグインをまとめたjsファイルを作ります。
http://tsudoi.org/photos/js/plugins.js
上記のjsファイルがデモページで使用しているファイルです。

・jQuery(v1.11.3)
・jQuery Easing(v1.3)
・kazitori.js(v1.0.2)
・createjs(PreloadJS)

上記のプラグインをまとめています。
createjsは、画像の先読みの際に使用しています。

続いて、実際にコンテンツを制御するjsファイルが、次のjsファイルになります。
http://tsudoi.org/photos/js/main.js
このjsファイルを見ながら解説していきます。
と、、、TypeScriptで実装しているので、実際に出力されるmain.jsを見ながらだと解説がわかり難いかもしれませんが、ご了承ください・・・。
解説に出てくるコードは、基本的にTypeScriptになります。

最初に、kazitori.jsを継承させたRouterクラスを生成します。

Routerクラスの生成
class Router extends Kazitori {
  public page(_pageName, _fileName){
		changePage(_pageName, _fileName, this.root);
  }
}
Router.prototype.notFound = '/photos/';
Router.prototype.root = '/photos/';
Router.prototype.routes = {
  '/' : 'page',
  '<a>' : 'page',
  '/<a>' : 'page',
  '<a>/<b>' : 'page',
  '/<a>/<b>' : 'page'
};

Routerクラスのpageメソッドの処理に関しては後述します。
notFoundプロパティと、rootプロパティには、’/photos/’の値を設定しています。
notFoundプロパティは、ページの登録がなかった場合に遷移するリンク先です。
rootプロパティは、基準となるディレクトリを設定します。

今回のデモページは、

http://tsudoi.org/photos/

になるので、’/photos/’を基準としました。
‘/photos/’を基準にしたので、htmlファイルのリンク設定はhtmlファイルがどの階層にいても’/photos/’を基準にしたリンク参照にする必要があります。

例えば、階層から見ると、2階層目になる次のページのグローバルナビゲーションのa要素のhref属性を見ていただくとわかると思いますが、
http://tsudoi.org/photos/arashiyama/index.html
全て’/photos/’を基準とした相対パスになっています。

最後に、routesプロパティですが、URLルールを設定するプロパティになります。
オブジェクトのキーの部分がルール値になっていて、値の部分がメソッドになります。

このルールの仕様については、Kazitori.jsの公式サイトで確認していただければと思います。
http://kazitori.org/

値に設定したメソッドをpageメソッドとしました。
これでページにアクセスした際に、pageメソッドが実行されることになります。
それでは、pageメソッドを見ていきます。

Routerクラスのpageメソッド
public page(_pageName, _fileName){
	changePage(_pageName, _fileName, this.root);
}

引数が2つ返ってきています。第一引数がディレクトリ名、第二引数がファイル名となります。
例えば、

http://tsudoi.org/photos/arashiyama/index.html

であれば、第一引数には”arashiyama”、第二引数には”index.html”となり、

http://tsudoi.org/photos/arashiyama/

であれば、第一引数には”arashiyama”、第二引数には”undefined”となります。

pageメソッドでは、そのまま引数を使用して、changePage関数を実行しています。
それでは、changePage関数を見ていきます。

Routerクラスのpageメソッド
function changePage(_pageName, _fileName, _root){
  var _page;
  if(_fileName){
    _page = _pageName + '/' + (_fileName || 'index.html');
  }else{
    _page = _pageName || '';
  };
  var _nowPage = window.pathname;
  var _nextPage = _root + _page;
  if( !( _nowPage === _nextPage ) || window.isFirstAccess ){
    window.isFirstAccess = false;
    $.when(
      $.setNextContents(_nextPage),
      $.exitPage()
    ).then( $.replaceContent );
  };
};

changePage関数で何をしているかというと、引数の値から次のコンテンツのURLを生成し、現在表示されているコンテンツのURLと比較し、異なるURLまたは初回アクセスであれば、jQueryのwhenメソッドを実行しています。
jQueryのwhenメソッドは、非同期処理を行うメソッドですが、この解説は割愛します。
ここでは、jQueryのsetNextContentsメソッド、exitPageメソッドを実行し、その後に、replaceContentメソッドを実行するような仕組みにしています。
これらのメソッドは、jQueryに追加させた独自のメソッドです。
それぞれを簡単に解説していきます。

setNextContentsメソッド
setNextContents: function(_pageName){
  _win.pageElement = _j('<div/>');
  var _def = _j.Deferred();
  return _j.ajax({
    url: _pageName,
    async: true,
    cache: false,
    dataType: 'text'
  }).done(function(e){
    _win.pageElement.empty().html(e);
    _win.pathname = _pageName;
    _j.replaceTitle( _win.pageElement.find('title') );
    _def.resolve();
  }).fail(function (){
    _def.reject();
  }), _def.promise()
}

setNextContentsメソッドでは、jQueryのajaxメソッドを使用して、次のコンテンツのhtml要素を取得しています。
取得したhtml要素は、windowオブジェクトに定義したpageElement変数に格納しておきます。
さらに、現在のtitle要素を取得したtitle要素に置き換えています。

exitPageメソッド
exitPage: function(){
  var _def = _j.Deferred();
  var _wrapper = _j('#wrapper');
  return _wrapper.stop().animate({opacity : 0},{
    duration : 300, easing: 'easeInSine',
    always : function(){
      _wrapper.css({visibility: 'hidden'});
      _def.resolve();
    }
  }), _def.promise()
}

exitPageメソッドでは、現在表示されているコンテンツのid=”wrapper”の要素をフェードアウトさせ、フェードアウト後、非表示設定にしています。

replaceContentメソッド
replaceContent: function(){
  var _def = _j.Deferred();
  var _element = _win.pageElement.find('#contents');
  var _contents = $('#contents');
  return _contents.empty(), _contents.replaceWith(_element), _contents.ready(function(){
		_j.initPage(), _def.resolve()
  }), _def.promise();
}

replaceContentメソッドは、setNextContentsメソッド、exitPageメソッドの実行後に実行されるメソッドです。
setNextContentsメソッドで取得した次のコンテンツのhtml要素からid=”contents”の要素を取得し、現在のコンテンツを置き換えています。
このタイミングで、コンテンツは切り替わっていますが、id=”wrapper”の要素は非表示になっているので、表示上は切り替わっていることはわかりません。
コンテンツが置き換わったタイミングで、initPageメソッドが実行されます。このinitPageメソッドもjQueryに追加させた独自のメソッドです。
それでは、initPageメソッドを見ていきます。

initPageメソッド
initPage: function(){
  var _def = _j.Deferred();
  var _loading = _j('#loading');
  _j.when(
    _j.getJSON(_win.json),
    _loading.fadeIn(300)
  ).then(function(_data){
    var _dataObj = _data[0];
    var _loadqueue = getLoadqueue();
    _loadqueue.on('complete', function(a){
      _loading.fadeOut(400, function(){
        _j.stopLoading();
        _win.base.trigger(a.type);
      });
      var _wrapper = _j('#wrapper');
      _wrapper.css({
        visibility : 'visible',
        opacity : '1'
      });
      _def.resolve();
    });
    _loadqueue.loadManifest(_dataObj);
  });
  return _def.promise();
}

initPageメソッドではまず、jQueryのgetJSONメソッドでjsonファイルを読み込んでいます。このjsonファイルはどこから来ているかと言うと、切り替わったコンテンツ要素内で定義しています。
例えば、

http://tsudoi.org/photos/arashiyama/index.html

上記のページでいうと、切り替わるコンテンツ要素は次のような内容になっています。

切り替わるコンテンツ要素
<div id="contents" class="contents category">
<script>
this.json = '/photos/json/pages/arashiyama.json';
</script>
<script src="/photos/js/pages/category.js" type="text/javascript"></script>
<ul id="photo-list">
<li><img src="/photos/imgs/arashiyama/0.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/1.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/2.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/3.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/4.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/5.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/6.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/7.jpg" alt=""></li>
<li><img src="/photos/imgs/arashiyama/8.jpg" alt=""></li>
</ul>
</div>

この切り替わるコンテンツ要素内に、json変数を定義し、そのページで使用される画像の参照先をまとめたjsonファイルを作成しておきます。
jsonファイルを読み込み、jQueryのfadeInメソッドでローディング要素をフェードインさせます。
この時、画面上では、ローディング画面となっています。
jsonファイルの読み込みが完了したタイミングで、getLoadqueue関数を実行し返り値を_loadqueue変数に格納し、_dataObj変数を引数にしてloadManifestメソッドを実行しています。
まず、getLoadqueue関数を見ていきます。

getLoadqueue関数
function getLoadqueue(){
  var _createjs = this.createjs;
  var _loadQueue = new _createjs.LoadQueue;
  _loadQueue.setMaxConnections(3);
  return _loadQueue;
};

getLoadqueue関数では、createjsのLoadQueueクラスを生成し、returnしています。
_loadqueue変数には、このLoadQueueクラスが格納されることになります。
loadManifestメソッドを実行する際に引数に代入した_dataObj変数には、jsonデータが格納されています。
これによって、jsonデータに格納されている画像参照先を全て読み込み、読み込み完了後、completeイベントを発生させています。
completeイベントでは、ローディング要素をフェードアウトさせ、フェードアウト後、非表示にしたあったid=”wrapper”の要素を表示させています。
画面上では、ローディング画面から次のコンテンツに切り替わっています。
さらに、windowオブジェクトに定義してあったbase要素のcompleteイベントを発生させています。
このbase要素のcompleteイベントはどこにあるかというと、これも切り替わったコンテンツ要素内で定義しているjsファイルにあります。
例えば、

http://tsudoi.org/photos/arashiyama/index.html

上記のページだと、

http://tsudoi.org/photos/js/pages/category.js

このjsファイル内に、completeイベントが登録されていますので、このcompleteイベントが実行されることになります。

これが一連の流れになります。
この記事だけではわかり難いと思いますので、実際に、デモページのソースを確認して解析などしていただければ把握できるかと思います。
文章だけでは解説は難しかったです・・・。

私自身まだHistory APIの知識がまだ薄く、今回のデモページもKazitori.jsという素敵なプラグインに頼ってしまったので、これからプラグインに頼らず独自に構築できるよう勉強しようと思います。