HTML5 History APIで非同期通信時にURLを変更する方法


HTML5 History APIで非同期通信時にURLを変更する方法

こんにちは、フロントエンドエンジニアの店長です。

先日記事が出てましたが改めて自己紹介します。
大学卒業後はカフェで仕事をしていたのですが、退職して1年半ほどWebデザイナーをしていました。そして、LIGにはフロントエンドエンジニアとしてジョインすることに。

お察しのとおり、店長というアダ名はカフェで働いていたためです。
今後ともよろしくお願いします。

さて、今回はHTML5のHistory APIについてお話したいと思います。

History APIについて

History APIには以前からブラウザの履歴(スタック)を行き来する機能があったのですが、HTML5でさらに以下のような機能が追加されました。

  • 画面を遷移せず、履歴に新たなURLを追加する。
  • 現在のページの履歴を変更する。
  • ブラウザの戻る・進むボタンをクリックしたときにイベントを検知する。

このような機能がどんな場面で使われているかというと、非同期通信でページ内を更新したときに、新たにURLを発行する場合です。
通常は非同期でページ内を更新した場合、URLは変わらないので、戻るボタンをクリックしたら最初の状態に戻ってしまいます。
そこで通信した際に新たなURLを発行してあげることで、通信後の状態をURLで残しておくことができるようになります。

今回は実際にデモを作りながら説明していきたいと思います。

DEMOについて

今回作るDEMOはjQueryでAjax通信を行って写真を取得するものです。
取得した際はHistory APIを使用して、履歴の追加を行います。
また、ブラウザの戻る・進むボタンがクリックされた際は、クリックした先のURLのデータを取得します。

DEMOはこちらからご覧になれます。

Ajaxで非同期通信する簡単なスライドを作る

まずはじめにAjaxで読み込みを行うデモを作りたいと思います。

ファイルの構成

├── css
│   └── style.css
├── data
│   ├── page1.html
│   ├── page2.html
│   ├── page3.html
│   └── page4.html
├── image
│   ├── 1.jpg
│   ├── 2.jpg
│   ├── 3.jpg
│   └── 4.jpg
├── js
│   └── app.js
├── page1.html
├── page2.html
├── page3.html
└── page4.html

各ページのHTMLファイルを用意し、dataフォルダの中には通信したときに取得する中身を用意しておきます。

 

<div class="container">
    <div class="photo" id="photo">
        <img src="image/1.jpg">
    </div>
    <div class="pager" id="pager">
        <li class="pager-num"><a class="is-current" href="page1.html">1</a></li>
        <li class="pager-num"><a href="page2.html">2</a></li>
        <li class="pager-num"><a href="page3.html">3</a></li>
        <li class="pager-num"><a href="page4.html">4</a></li>
    </div>
</div>

ページ内の構成は写真とページャーというシンプルな構造です。

JavaScript

$(function(){
    var BASE_PATH = '/history-api/data/';
    var request = null;
    var $pager = $('#pager').find('a');

    //ページャーをクリックしたとき
    $pager.on('click', clickHandler);

    //クリックした際に実行される関数
    function clickHandler(e){
        e.preventDefault();
        var self = this;
        var page = $(self).attr('href');
        //ページャーの変更
        changePager.apply(self);
        //データをロードする
        loadingData(BASE_PATH + page);
    }

    //ページャーを変更する
    function changePager(){
      $pager.removeClass('is-current');
        $(this).addClass('is-current');
    }

    //データを取得する
    function loadingData(url){
        //requestを確認してabort
        if(request){
            request.abort();
        }
        //データのロード
        request = $.ajax({
           type: 'GET',
           dataType: 'html',
           url: url
        }).done(function(data){
            $('#photo').html(data);
        });
    }
});

まずは、ページャーをクリックしたらそのページの情報を読み込むようにします。
page1.htmlの場合はdata/page1.htmlの情報を取得します。
通信はjQueryの$.ajaxで行います。

クリックしたときはrequestという変数の中身をチェックし、$.ajaxが含まれている場合は.abord()メソッドを使用して、現在通信してる分をキャンセルします。
こうすることで余計なリクエストを減らすことができます。

pushState()で履歴を追加する

では、この中に、クリックされたとき新たに履歴を追加する機能を作っていきましょう。

履歴を追加するにはhistory.pushState()を使用します。

history.pushState(state, title, url);

state

任意のオブジェクトを渡すことができます。渡したオブジェクトはpopStateイベントハンドラで参照が可能です。

title

タイトルを指定できますが、現在は特に使われていません。

url

追加するURLを指定します。

今回はstate、titleは特に使用しないのでnullを指定しておきます。
ページャーがクリックされたらページを移動したいので、ここにコードを追加します。

//クリックした際に実行される関数
function clickHandler(e){
    e.preventDefault();
    var self = this;
    var page = $(self).attr('href');
    //履歴の追加
    history.pushState(null, null, page);
    //ページャーの変更
    changePager.apply(self);
    //データをロードする
    loadingData(BASE_PATH + page);
}

ただ、このままだと現在のページと同じURLをクリックした際もURLを追加していしまうので、それを回避します。

 

//クリックした際に実行される関数
function clickHandler(e){
    e.preventDefault();
    var self = this;
    var page = $(self).attr('href');
    var currentPage = getCurrentPage();
    //クリックしたリンク先のURLと現在のURLが一致する場合は処理を終える
    if(page === currentPage){
        return;
    }
    //履歴の追加
    history.pushState(null, null, page);
    //ページャーの変更
    changePager.apply(self);
    //データをロードする
    loadingData(BASE_PATH + page);
}

//現在のページ名を取得する
function getCurrentPage(){
  return location.pathname.split('/').pop();
}

最初にcurrentPageで現在のページ名を取得し、ページャーがクリックされたらcurrentPageとクリックされたリンクのhrefを比べます。
もし同じ場合はreturnして処理を終えます。
違う場合はcurrentPageに新たにパスを追加して、pushStateで履歴(スタック)にも追加を行います。

popStateイベントで戻るボタンがクリックされたことを検出する

pushState()で非同期通信時に履歴を追加することができました。
しかし、現在の状態だと戻るボタンを押しても反応がありません。

//戻る・進むボタンがクリックされたとき
$(window).on('popstate', popstateHandler);

//戻る・進むボタンがクリックされたときに実行される関数
function popstateHandler(e){
    e.preventDefault();
    var currentPage = getCurrentPage();
    $pager.each(function(){
        var self = this;
        var page = $(self).attr('href');
        if(page === currentPage){
            changePager.apply(self);
        }
    });
    loadingData(BASE_PATH + currentPage);
}

そこで、ブラウザの戻る・進むボタンがクリックされた際のイベントを検出します。
イベントを検出するにはpopStateイベントを使用します。

 

$(function(){
    var BASE_PATH = '/history-api/data/';
    var request = null;
    var $pager = $('#pager').find('a');

    //ページャーをクリックしたとき
    $pager.on('click', clickHandler);
    //戻る・進むボタンがクリックされたとき
    $(window).on('popstate', popstateHandler);

    //クリックした際に実行される関数
    function clickHandler(e){
        e.preventDefault();
        var self = this;
        var page = $(self).attr('href');
        var currentPage = getCurrentPage();
        //クリックしたリンク先のURLと現在のURLが一致する場合は処理を終える
        if(page === currentPage){
            return;
        }
        //履歴の追加
        history.pushState(null, null, page);
        //ページャーの変更
        changePager.apply(self);
        //データをロードする
        loadingData(BASE_PATH + page);
    }
    
    //現在のページ名を取得する
    function getCurrentPage(){
      return location.pathname.split('/').pop();
    }

    //戻る・進むボタンがクリックされたときに実行される関数
    function popstateHandler(e){
        e.preventDefault();
        var currentPage = getCurrentPage();
        //ページャーの変更
        $pager.each(function(){
            var self = this;
            var page = $(self).attr('href');
            if(page === currentPage){
                changePager.apply(self);
            }
        });
        //データをロードする
        loadingData(BASE_PATH + currentPage);
    }

    //ページャーを変更する
    function changePager(){
      $pager.removeClass('is-current');
        $(this).addClass('is-current');
    }

    //データを取得する
    function loadingData(url){
        //requestを確認してはabort
        if(request){
            request.abort();
        }
        //データのロード
        request = $.ajax({
           type: 'GET',
           dataType: 'html',
           url: url
        }).done(function(data){
            $('#photo').html(data);
        });
    }
});

jQueryのonメソッドを使用してpopStateイベントを検出します。
イベントが発火したら現在のページ名を取得して、そのページのデータを取得します。一緒にページャーも変更します。

以上で完成です。

おまけ

HTML5 History APIには他にも機能があるので簡単にご紹介します。

replaceState

replaceState(state, title, url);

replaceStateは現在の履歴を置き換えることができます。
引数はpushStateと同じです。

replaceStateとpushStateの違いを調べるためのDEMOを作りました。
コードは以下の通りです。

$(function(){
  var $replace = $('#replace');
  var $push = $('#push');
  var before = null;
  var after = null;
  var name = null;

  //replaceStateボタン
  $replace.on('click', function(){
    clickHandler(runReplaceState);
  });

  //pushStateボタン
  $push.on('click', function(){
    clickHandler(runPushState);
  });

  //クリック時実行する関数
  function clickHandler(callback){
    before = history.length;
    name = callback();
    after = history.length;
    alertDetial();
  }

  //pushState()を実行する
  function runPushState(){
    history.pushState(null, null, '#push');
    return 'pushState()';
  }

  //replaceState()を実行する
  function runReplaceState(){
    history.replaceState(null, null, '#push');
    return 'replaceState()';
  }

  //alert()を実行する
  function alertDetial(){
    alert(name + 'が実行されました。\nボタンを押す前の履歴の数は' + before + '\nボタンを押した後の履歴の数は' + after + 'です。');
  }
});

history.lengthで履歴の数を調べ、replaceStateを実行する前とした後の変化を調べます。

それぞれのボタンをクリックするとpushState、replaceStateが実行されます。
pushStateは履歴が追加されるので、実行する前と後の履歴の数が変わります。
replaceStateは履歴を置き換えるので、実行する前と後の履歴の数は変わりません。

history.state

pushStateとreplaceStateの第一引数のstateはhistoryオブジェクトで確認することができます。

replaceState('hoge', null, 'page1.html');
console.log(history.state); //hoge

ブラウザの対応状況について

残念ながらIE8、IE9はサポートしていません。
Androidはすこし不思議で、2.2、2.3では対応していたのですが、Android3から対応を取りやめしてしまい、そして4.2でまた復活しています。

もし未対応のブラウザでもpushState、replaceStateなどを使いたいという要望があれば、History.jsでフォールバック対応ができます。

History.jsでは、HTML5 History APIが使用可能か調べ、使用不可であればハッシュを使用して擬似的にStateを実装してくれます。

まとめ

いかがでしたでしょうか?
このような処理を行うことでユーザがよりサイトを使いやすくなる場面もあると思います。
非同期通信する際はHistory APIを使ってみてください。

 

【APIでできることを知ろう!】

WebAPIドキュメント作成サービス「Apiary」を使ってWebアプリ開発を高速化しよう

Instagram APIを取得してWebサイトと連携し、投稿写真を自動に掲載する方法

Web初心者でもGoogle Mapsをカスタマイズできるgmaps.jsでAPIを使い倒そう!

画像を解析して写真にタグ付けをするAPI「clarifai」のデモを試してみた

Node.jsからGoogle Analytics APIにアクセスし、GA情報を表示させよう

この記事を書いた人

店長
店長 フロントエンドエンジニア 2015年入社
フロントエンドエンジニアの店長です。
LIGに入社と同時に店長(あだ名が)になりました。偉くはありません。
以前、某カフェで働いていました。

音楽とコーヒーが大好きです。よろしくお願いいたします。