uhyohyo.net

JavaScript初級者から中級者になろう

十二章第三回 History API

このページの最終更新日:

次に紹介するのがHTML5 History APIです。Historyとは履歴のことです。履歴関係のことは、historyという名前のオブジェクトから利用可能です。

古典的な履歴操作

実はこのhistory自体は昔からあって、いくつかメソッドを持っていました。


history.back();

例えばhistory.backメソッドは、いわゆるブラウザの「戻る」ボタンと同じ動きです。

実際にやってみましょう。

このボタンは次のようにできています。


  <input type="button" value="戻る" onclick="history.back()">
  

このボタンを押すと、ブラウザの「戻る」ボタンを押したときと同じように前のページに戻ります。

逆に「進む」はhistory.forward()です。他に、「3個すすむ」とか「2個戻る」とかを汎用的に表現するhistory.goがあります。これは引数を取って、使いかたはこうです。


history.go(3);	//3個すすむ
history.go(-2);	//-2個戻る

このように、正の数値は進む、負の数値は戻るを表す数値を引数としてとります。

以上が、昔からある古典的な履歴操作です。次に、HTML5で加わった機能を説明します。

履歴の追加

HTML5での画期的な新機能は何かというと、履歴の追加です。勝手に自分で履歴を追加できるのです。

例えば、を押してみましょう。

押すと、実際にはページを移動していないのに、上のURLが「https://uhyohyo.net/javascript/testtest」というように変わったと思います。もちろん、実際にそんなURLを開いても何もありません。

さらに、そこからブラウザで「戻る」をやってみましょう。またもページが変わらずに、URLが元に戻ったと思います。これが意味するのは、このボタンによって、実際にページが変わったわけではないけれども「https://uhyohyo.net/javascript/testtest」というURLに行ったという履歴が追加されたということです。「進む」を押してみるとまたURLが変わるはずです。

早速その方法を紹介します。使うのは、history.pushStateメソッドです。例えば次のように呼び出します。

history.pushState(null, 'テスト', '/javascript/testtest');

3つの引数があることがわかりますね。history.pushStateは、呼び出されると新しい履歴を追加してその履歴に遷移します。

順番が前後しますが、第3引数が新しい履歴のURLだということが分かると思います。また、第2引数は履歴のタイトルです。ただ、タイトルは現状のブラウザでは意味がないようです。第3引数については省略可能であり、その場合は現在のURLと同じになります。

残る第1引数はstateであり、これはその履歴と関連付けられた情報です。好きなオブジェクトを履歴と関連付けることができます。stateの使い方については後述します。ただ、今回は履歴と関連付けたい情報は特に無いのでnullにしてあります。

ちなみに、「履歴を追加」ではなく「現在の履歴を上書き」してしまう、history.replaceStateもあります。使い方はpushStateと同じです。

履歴の活用

さて、これで新しい履歴を追加する方法が分かりました。ちなみに、追加した履歴を消す方法はありません。

ところで、実際にページが移動するわけでもない履歴を追加して何の役に立つのでしょうか。

これは、近年発達してきているシングルページアプリケーションSPA)を作るときに役立ちます。SPAとは、ひとつのページ内で完結しているウェブアプリケーションのことです。つまり、リンクやボタンをクリックして他の画面に移動したように見えても、実はJavaScriptが画面を書き換えただけでページは移動していないというものです。

このようなユーザーインターフェースにおいて、ユーザーがブラウザの「戻る」ボタンで前の画面に戻ろうとするのは自然な行動です。しかし、この場合「戻る」の動作として適切なのは実際に前のページに戻ることではなく、再びJavaScriptによって画面が書き換えられて前の画面に戻るという動作です。History APIの真髄はこの動作を実現することにあります。そのためには、これまで紹介した履歴の追加に加えて、いまから紹介する履歴移動の検知を用いることになります。

具体的には、popstateというイベントを用いることで履歴移動を検知することができます。これは履歴を移動したら発生するイベントです。ただし、別のページへ行ってしまう場合は発生しません。pushStateなどによって追加された履歴の間で(すなわち、同じページの中で)履歴を移動する場合に発生してくれるイベントです。

popstateイベントはwindowで発生するイベントなので、このように検知します。


window.addEventListener('popstate', function(ev){
}, false);

しかし、「履歴を移動した」だけでは情報が足りません。そこで役に立つのがさっきのstateです。移動後の履歴に関連付けられたstateオブジェクトがイベントオブジェクトのstateプロパティに入っています。つまり今回の場合はev.stateですね。これを用いれば、移動後の履歴がどういう状態なのかを判別できるわけです。

さて、それではこれらを活用して具体的に何ができるかを見ていきたいと思いますが、前回に続きサンプルを提示したいと思います。

今回作ってみるのは、いわゆるタブっぽいUIを持ったページです。タブを選択すると表示内容が切り替わります。まずはHistory APIを使っていない土台のサンプルを見てください。上に3つリストがあって、クリックするとタブA,タブB,タブCが切り替わります。ちなみに随所でWAI-ARIAを使っていますが、今回の解説と本質的にはそんなに関わりませんので、気にしないでください。

このサンプルでは、何度かタブを切り替えたあとにブラウザの「戻る」ボタンを押すと当然もとのページに戻ります。しかし、前に選択していたタブに切り替わるほうが直感には合っているのではないでしょうか。

History APIを使えばこれを実現することができます。1回タブを開くごとに新しい履歴を追加すればいいのです。

これをやってみたのが次のサンプル2です。少し長いですがソースを見てみるとよいでしょう。

タブを切り替えるとhistory.pushStateで履歴を追加します。その際、どのタブを開いたかはstateに入れることにしています。

popstateが発生した場合はstateを見て、それによりどのタブが開いている履歴に戻ったのか判断します。

最後にhistory.stateを紹介します。これは「現在の履歴」に関連付けられたstateが入っています。

location

次に紹介したいのはハッシュです。ハッシュとは、


http://uhyohyo.net/index.html#abc

というような、URLにおける「#」以降の部分です。これは一般的にページ名には含まれず、あるページ内での位置を表すなどするのでした。

JavaScriptでは、このハッシュを用いた操作をすることができます。それには、locationというオブジェクトを使います。

locationもわりと昔からあり、URLを扱うためのものです。例えば、 location.href で現在のURLを取得できます。さらに、このlocation.hrefに代入すると、そのページへ移動することができます。

locationには、URLの各部分をいじることができるプロパティがあります(後述)。ハッシュの部分だけを変更するには、location.hashを使います。例えば先程の例だと、location.hashには"#abc"と入っています。これに代入して変更すると、やはりURLが変わります

URLの部分の中でもハッシュというのは特殊な性質があります。これは一般にページ内での位置を表すものです。なので、ハッシュが変更されてもページは変わらないという性質があります。しかし、ハッシュはURLに含まれますからURLは変わります。この、ページが変わらないのにURLが変わるという挙動はpushStateと似ていますね。実際のところ、ハッシュを変更すると新しい履歴が追加されます。ということは、「戻る」ボタンが押されると同じページの中でハッシュだけが違うURLに移動することになります。同じページ内での履歴移動ということはpopstateイベントも発生します。

さっきのタブの例を、pushStateではなくlocation.hashを用いるように変更したのがサンプル3です。

変更点は、pushStateの部分がlocation.hashへの代入になった点と、popStateイベントの発生時にev.stateではなくlocation.hashを調べている点です。

ここで注目すべきことは、popStateが呼び出された時点でlocationのURL情報は履歴移動後のものになっているということです。

hashchangeイベント

ところで、popstateと似たイベントとして、履歴移動のうちハッシュだけが変わった場合に発生するhashchangeイベントがあります。つまり、pushStateの第三引数によってURLも変わってしまった場合の履歴移動などでは発生しないイベントということです。

hashchangeイベントのイベントオブジェクトにはoldURLnewURLという2つのプロパティがあり、移動元・移動後のURLを両方得ることができます。ただし、先程述べたとおり、ハッシュ以外は変わっていません。

ということで、最後に、popstateの代わりにこのhashchangeイベントを利用したサンプル4を提示します。今回は、このoldURL・newURLは特に使う必要もないので使っていません。popstateのときと同様にlocation.hashを参照しています。

特筆すべきは、さっきのサンプル3ではlocation.hashに代入するときにopenTab関数を呼び出していたのに、サンプル4ではそれが消えているということです。

これはなぜかというと、hashchangeイベントが発生するのは「ハッシュが変わった場合」ですので、履歴移動により変わった場合はもちろんのこと、実は「JavaScript側によって変えられた場合」にも呼ばれるのです。ですから、location.hashに代入してハッシュが変わったので、その時点でhashchangeイベントが呼ばれているのです。

このようにhashchangeイベントを適切に使うと、今回の場合はopenTabを呼び出す箇所が一回だけに減るなど、すっきりしたコードが書けます。

また、ハッシュを用いて履歴を管理することには、場合によってはもうひとつ大きな利点があります。それは、ページの状態とURLが対応するという点です。例えば上記のサンプル4では、「タブB」を開いた状態のURLはhttp://uhyohyo.net/javascript/12_3_sample4.html#tab_Bとなり、「タブC」を開いている場合はhttp://uhyohyo.net/javascript/12_3_sample4.html#tab_Cとなります。つまり、URLに「今どのタブを開いているか」という情報が入っています。このことは、URLからページの状態を復元可能であることを意味します。例えばユーザーが「タブB」を開いた状態のURLを保存しておいて、あとでそのURLにアクセスするとタブBが開いている、というようなことが可能です。

しかし、実は上のサンプル4はそうなっていません。このことを簡単に確かめる方法は、タブBやタブCを開いた状態でページの更新(再読み込み)を行うことです。現状のサンプル4では、この場合も初期状態と同様タブAが開いてしまいます。

これを直すのは簡単です。初期状態(ページを開いた状態)でハッシュが読みどのタブを開くか決定してあげればいいのです。練習問題としてやってみましょう。

locationのまとめ

最後に、今回ちらりと紹介したlocationオブジェクトについて以下にまとめます。


//プロパティ(すべて代入して変更可能です)
location.href;		//例: "http://uhyohyo.net/javascript/12_3.html?key=value#abc"
location.protocol;	//例: "http:"
location.host;		//例: "uhyohyo.net"
location.hostname;		//例: "uhyohyo.net"
location.port;		//例: ""
location.pathname;	//例: "/javascript/12_3.html"
location.search;	//例: "?key=value"
location.hash;		//例: "#abc"

protocolは、「:」まで入っていることに注意してください。また、portは、通信に使用するポートです。HTTP通信は普通80で、この場合URLからポートは省略されます。ポートが省略されていないURL(例:"http://example.com:8080/")の場合には、文字列で"8080"などと入ります。

hostとhostnameの違いは、ポート部分を含むかどうかです。hostnameは含めませんが、hostは含めます。

また、locationはlocation.assignというメソッドがあります。これは引数をひとつとってそのURLへ移動するメソッドで、location.hrefに直接代入するのとあまり変わりません。

また、location.replaceというメソッドがあって、これはassignと同様の使い方ですが現在のページの履歴情報を上書きするもので、これを使ってページを移動すると「戻る」を押しても移動前が履歴に残っておらず、さらにその一つ前に飛ぶことになります。

また、location.reloadは引数なしのメソッドで、現在のページを「更新」します。