❧
ブラウザのロケーションバーは世にある UI のなかで最もギークなもののひとつだろう。URL はビルボードや電車の車体、はたまたグラフィティにまで現れる。戻るボタン (ブラウザでもっとも重要なものだ) と組み合わせれば、Web と呼ばれる複雑に絡みあったリソースの集合を行き来するとても強力な手段となる。
HTML5 の History API はブラウザのヒストリ情報をスクリプトから操作する機能だ。この API の一部にはヒストリを行き来する機能があるが、これは以前の HTML の頃より存在していた。HTML5 では、ブラウザのヒストリにエントリを追加する機能、ページの更新なしにロケーションバーの URL を書きかえる機能、ユーザーが戻るボタンを押しそのエントリがスタックから削除される際に発火されるイベントなどが新しく追加された。これによって、ロケーションバー中の URL は、ページの全部を更新しないようなスクリプトごりごりのアプリケーションにおいても、現在のリソースに結び付けられた一意な識別子という性質を維持できるのだ。
❧
一体なぜ、ブラウザのロケーションバーを操作する必要があるのだろうか。リンクから別の URL へ飛ぶ。これはその始まりから今日までの 20 年間ずっと続く、Web のもっとも基本的な仕組みだ。そして、これからも変わることはない。この API は Web を破壊するものなんかではないのだ。むしろその逆だ。近年、Web 開発者は Web をひっくり返す面白い方法を、標準の助けなしに見つけてしまった。HTML5 の History API は、スクリプトごりごりの Web アプリケーションでも URL が変わらず機能し続けることを保証するために用意されているのだ。
最初の原則に立ち返ってみよう。URL はなにをするものだろうか。リソースを一意に識別するものだ。この役割のおかげで、リソースを直接リンクすることやブックマークすることができる。検索エンジンはリソースをインデックスできる。URL をコピーしメールに貼りつけ誰かに送れば、送り相手はそれをクリックしてあなたが見たリソースと同じものを見られる。そんなすばらしい役割があるのだ。URL は偉大だ。
一意なリソースには一意な URL があってほしいものだ。しかし、ブラウザには根本的な欠陥がある。URL を変更すると、たとえスクリプトを使ったとしても、変更したことで Web サーバへの接続が行われ、ページが更新されてしまうのだ。これは時間やリソースを使うし、なにより現在のページが、移動先のページとほとんど同じである場合はとても無駄になる。移動先のページにあるすべてがダウンロードされてしまう。URL を変えて、でもページの半分しかダウンロードするなとブラウザに伝える、そんな方法は存在しなかった。
HTML5 の History API を使えば、それを実現できる。ページを完全に更新するのではなく、基本的にはスクリプトを使って部分的にページをダウンロードするのだ。このイリュージョンを見破るのは難しいし、ページにタネを仕込む必要もある。ではご覧いただこう。見逃さないように。
ここに A と B という2つのページがある。2つのページの90%は同じ内容だが、残りの10%はそれぞれ異なっている。ユーザーはページ A にアクセスした後、ページ B にアクセスする。このとき、ページを更新するのではなく、そのページ遷移を妨害し次に記すステップを実行する。
XMLHttpRequest
を使って、ページ A とページ B の異なる部分だけをロードする。これをするにはサーバーサイドに多少の変更が必要になるだろう。ページ A とページ B の異なる10%の部分だけを返すコードを書くのだ。実装は隠れた URL やクエリパラメータなど、エンドユーザーが普段見ないものでよいだろう。
innerHTML
や他の DOM メソッドを利用するだろう。) 入れ替わった内容のイベントハンドラをリセットしないといけない場合が出てくるかもしれない。
このイリュージョンの終わりには、ブラウザはページ B に直接移動したかのように、そのページと同じ DOM を持つことになる。ロケーションバーも同じくページ B の URL を持つ。しかし、実際にはページ B にアクセスしてはおらず、そしてページは更新されもしない。これがマジックなのだ。しかし「合成した」そのページはページ B と同じに見えるし、URL もページ B と同じだ。ユーザーがその違いに気づくことはないだろう (その体験を提供するためにあなたがしたことに感謝することもないだろう)。
❧
HTML5 の History API は window.history
オブジェクトにいくつか用意されたメソッドと window
オブジェクトに追加された1つのイベントから構成される。これらを利用すれば History API のサポートを検出 できる。サポートはいくつかのブラウザの最新バージョンに限定されるため、“Progressive Enhancement” な使い方が前提となる。
IE | Firefox | Safari | Chrome | Opera | iPhone | Android |
---|---|---|---|---|---|---|
10+ | 4.0+ | 5.0+ | 8.0+ | 11.50+ | 4.2.1+ | 2.2+* |
* Android ブラウザは Android 2.2 で History API をサポートしたが、Android 3.x, Android 4.0 ではサポートが無効にされている。 |
dive into dogs は何の変哲もないが、作るのはそこまで簡単でない、HTML5 の History API を利用したサンプルだ。長めの記事と、インラインのフォトギャラリーというよくあるパターンで構成されている。サポートされたブラウザでは、フォトギャラリー中の Next と Previous というリンクをクリックすると、写真が置き換えられ、さらにブラウザのロケーションバーにある URL も更新される。しかし、ページ全体が更新されるわけではない。サポートされていないブラウザや、スクリプトを無効にしたブラウザにおいて、これらのリンクは通常のリンクとして扱われるため、新しいページに移動することになる。もちろん、新しいページなので、全体が更新される。
最後の2文が、とくに重要だ。
スクリプトを無効にしたブラウザで君の Web アプリケーションが動作しなかったら、どこからともなくニールセン博士の犬が君のところにやってきて、カーペットに粗相でもするだろうね。
では、dive into dogs のデモを詳しく見ていこう。写真を表示している箇所のマークアップは次のようになっている。
↶ The pledge
<aside id="gallery">
<p class="photonav">
<a id="photonext" href="casey.html">Next ></a>
<a id="photoprev" href="adagio.html">< Previous</a>
</p>
<figure id="photo">
<img id="photoimg" src="gallery/1972-fer-500.jpg"
alt="Fer" width="500" height="375">
<figcaption>Fer, 1972</figcaption>
</figure>
</aside>
何らおかしなところは見当たらない。写真は <figure>
内の <img>
で、リンクは普通の <a>
要素、写真やそのコントロールは <aside>
で囲まれている。リンクが普通に機能することは重要だ。次に記すすべてのコードは 検出スクリプト のもと機能する。サポートしないブラウザでは、History API のコードは実行されない。スクリプトを無効にしたユーザーは、かならず一定数存在するのだ。
スクリプトのメイン関数は、上記のリンクを取得し addClickr()
という click
ハンドラを独自に設定する関数に渡す作業を行う。
function setupHistoryClicks() {
addClicker(document.getElementById("photonext"));
addClicker(document.getElementById("photoprev"));
}
これが addClicker()
関数だ。この関数は <a>
要素を受け取り、それに click
ハンドラを与える。この click
ハンドラの中身から、スクリプトが面白くなってくる。
function addClicker(link) {
link.addEventListener("click", function(e) {
swapPhoto(link.href);
history.pushState(null, null, link.href);
e.preventDefault();
}, false);
}
↜ Interesting
swapPhoto()
関数が、先ほど説明した 3ステップのイリュージョン のうち、1番目と2番目を担当する。swapPhoto()
の前半分は、ナビゲーションリンクの URL の一部 (casey.html
や adagio.html
など) を受け取り、次の写真を表示するために必要なマークアップのみを持ったページの URL を作成する。
function swapPhoto(href) {
var req = new XMLHttpRequest();
req.open("GET",
"http://diveintohtml5.info/examples/history/gallery/" +
href.split("/").pop(),
false);
req.send(null);
次に示すのは http://diveintohtml5.info/examples/history/gallery/casey.html
が返すマークアップだ。(URL に直接アクセスすれば、ちゃんと同じものが帰ってくるかを確かめることができる。)
<p class="photonav">
<a id="photonext" href="brandy.html">Next ></a>
<a id="photoprev" href="fer.html">< Previous</a>
</p>
<figure id="photo">
<img id="photoimg" src="gallery/1984-casey-500.jpg"
alt="Casey" width="500" height="375">
<figcaption>Casey, 1984</figcaption>
</figure>
見覚えがあるのではないだろうか。そう、このマークアップは1枚目の写真を表示する もとのページのマークアップと基本的に同じもの だ。
swapPhoto()
関数の後半が、イリュージョンのうち2番目のステップを実行する。新しくダウンロードしたマークアップを現在のページに挿入するのだ。先ほどのコードを覚えているだろうか。ナビゲーション、写真、キャプションは <aside>
に囲まれている。なので、新しい写真の挿入は XMLHttpRequest
から返ってきた responseText
プロパティを、<aside>
の innerHTML
プロパティに設定するだけで済む。
if (req.status == 200) {
document.getElementById("gallery").innerHTML = req.responseText;
setupHistoryClicks();
return true;
}
return false;
}
(setupHistoryClicks()
を呼び出していることにも注目だ。これは新しく挿入されたナビゲーションリンクの click
イベントハンドラをリセットするものだ。innerHTML
をセットすると、古いリンクの痕跡やそのリンクに与えられたイベントハンドラが消去される。)
addClicker()
関数に戻ろう。さて、これまで説明したのは 3ステップのイリュージョン なのを覚えているだろうか。つまり、写真を入れ替えることに成功したあと、もうひとつ実行しなければならないステップがあるのだ。ページを更新せずに、ロケーションバーの URL をセットするのだ。
↶ The turn
history.pushState(null, null, link.href);
history.pushState()
関数は3つの引数をとる。
state
― JSON データ構造であるものであれば何でもよい。オブジェクトはこの後すぐ説明する popstate
イベントハンドラに渡される。このデモでは何の状態も追跡する必要がないので、ここでは null
としている。
title
― どんな文字列でもよい。この引数は現時点でどのブラウザでも利用されていない。ページタイトルをセットしたい場合は、state
引数に格納し、popstate
のコールバックから手動で指定するしかない。
url
― どんな URL でもよい。ここに指定したものが、ロケーションバーに現れる URL となる。
history.pushState
を呼ぶとすぐさまロケーションバーの URL が変更される。では、これでイリュージョンは終わりだろうか。残念ながらそうではない。ユーザーがあの重要な戻るボタンを押した時の挙動について考えなくてはならないからだ。
ユーザーが新しいページへ移動した時 (ページも更新されたとき)、ブラウザはヒストリのスタックに URL をプッシュし、ページをダウンロードし、そして描画する。ユーザーが戻るボタンを押すと、ヒストリスタックからページをポップし、前のページを再描画する。しかし、ページの更新なしにページを入れ替えた今の場合において、ユーザーが戻るボタンを押した際にはどうなるのだろうか。つまり、サンプルでは「進む」挙動を偽証し、新しい URL に移動している。つまり、「戻る」についても同様に、前の URL へ移動する際に挙動を偽証しなければいけないのだ。そしてその鍵は、popstate
イベントが握っている。
↶ The prestige
window.addEventListener("popstate", function(e) {
swapPhoto(location.pathname);
}
history.pushState()
関数を使い、ブラウザのヒストリスタックに偽の URL をプッシュした後、ユーザーが戻るボタンを押すと、ブラウザは popstate
イベントを window
オブジェクトから発火する。ここでちゃんとするかしないかで、イリュージョンの成功が決まってしまう。何かを消すだけでは成功とは言えない、それをもとに戻してこそイリュージョンだろう。
今回のデモにおいて「もとに戻す」ことは簡単だ。swapPhoto()
にもとの URL を与え、もとの写真を呼び出せばよいだけだ。popstate
コールバックが呼び出されるころには、ロケーションバーの URL は前の URL に変わっているだろう。また、location
プロパティもすでに前の URL に更新しているだろう。
では、イリュージョンがどう起こったのかを、その最初から最後までまとめて説明しよう。
http://diveintohtml5.info/examples/history/fer.html
にアクセスし、本文と Fer の写真を見る。
href
プロパティに http://diveintohtml5.info/examples/history/casey.html
を持つ <a>
要素だ。
http://diveintohtml5.info/examples/history/casey.html
への移動とページの更新が発生する代わりに、<a>
要素に与えられた click
ハンドラがもとのクリックを妨害し、自前のコードを実行する。
click
ハンドラは swapPhoto()
関数を実行する。この関数は XMLHttpRequest
オブジェクトを生成し、 http://diveintohtml5.info/examples/history/gallery/casey.html
にある HTML の一部を同期的にダウンロードする。
swapPhoto()
関数はフォトギャラリーの囲み (<aside>
要素) の innerHTML
プロパティを入れ替える。この場合は Fer の写真とキャプションを、Casey の写真とキャプションに変更する。
click
ハンドラは history.pushState()
関数を呼び出し、ロケーションバーの URL を http://diveintohtml5.info/examples/history/casey.html
に書きかえる。
history.pushState()
によって) ヒストリスタックに URL が手動でプッシュされたことに気づく。もとの URL への移動とページ全体の再描画が発生する代わりに、ブラウザはロケーションバーをもとの URL (http://diveintohtml5.info/examples/history/fer.html
) に書き換え、そして popstate
イベントを発生させる。
popstate
ハンドラが swapPhoto()
を再度呼び出す。引数には、ロケーションバーではすでに書き変わっている、もとのページの URL が与えられる。
swapPhoto()
関数は XMLHttpRequest
を使用し http://diveintohtml5.info/examples/history/gallery/fer.html
にある HTML の一部をダウンロードし、その内容を <aside>
要素の innerHTML
プロパティにセットする。Casey の写真とキャプションが、Fer の写真とキャプションに変更される。
イリュージョンは成功だ。ページ内容とロケーションバーの URL を見ると、まるでユーザーはあるページから別のページを行き来しているようだ。しかし、ページ全体が更新されているわけではない。これは細かいトリックを組み合わせたイリュージョンなのだ。
❧
❧
訳註:このページは Dive Into HTML5 の “Manipulating History for Fun & Profit” の日本語訳です。
原文が公開されている Dive Into HTML5 というサイトは “HTML5: Up & Running” という名前で Google Press より出版、O’Reilly Media より発売されています。(ePub, Mobi, DRM-free PDF など紙以外の媒体でも販売されています。)
原著を購入したい方はぜひ 著者のアフィリエイトリンク から、もしくは O’Reilly より電子版を購入 してください。
訳註:“HTML5: Up & Running” の日本語訳はオライリー・ジャパンより「入門 HTML5」として発売されています。(本ページの訳者が監訳として関わっていますが、この翻訳文書は書籍と無関係です。)
訳註:翻訳元の内容は “HTML5: Up & Running” に含まれていません。同様に、この日本語訳も「入門 HTML5」に含まれていません。
Copyright MMIX–MMXI Mark Pilgrim
翻訳: Masataka Yakura