August 2012

Volume 27 Number 08

HTML5 アプリケーションをビルドする - History (API) のレッスン

Clark Sell | August 2012

Web ブラウザーに関連して履歴といえば、通常は戻るボタンを意味し、簡単に管理できるものではありません。Web アプリケーションで AJAX の重要性が増し、標準的な Web サイトからリッチな Web アプリケーションへと成長を始めると、こうした管理はますます難しくなります。これまではどちらかといえば無効にすることが多かった JavaScript が既定で有効になるようになったため、クライアント側で JavaScript を多用する 1 ページの Web アプリケーションも現れました。しかし、クライアント側のストレージやデータベース、ましてやページ間の一般的な状態を扱う優れた方法はありません。

しかし、HTML5 ではクライアント側の状態管理に正面から取り組み、IndexedDB、Local Storag、History API などの仕様を含む、まったく新しい一連の API を導入するようになりました。今回はこの中から History API に注目します。

History API の目標を簡潔に要約すると、リッチな JavaScript アプリケーションからセッション履歴を適切に移動できるようにするだけでなく、セッションの状態も管理できるようにし、URL のサポートに優れた手段を提供することです。つまり、シンプルな API 呼び出しとイベントをいくつか使えば、URL の優れた構造を維持したまま、戻るボタンの動作方法などに作用する状態データをセッション スタックにプッシュ、ポップして、セッション履歴を移動することができます。

問題点

解決策に取り組むときは、問題点を理解することが重要です。History API は、エンド ユーザーが戻るボタンや進むボタンを押したときに、ページの状態を保存するためだけのものではありません。確かにこれも機能の 1 つですが、本質はもっと深いところにあります。ユーザーから見ると、ブラウザーにはページを移動する主な機能として、URL とナビゲーション ボタン (進むと戻る) の 2 つがあります。ユーザーはこの 2 つの機能を組み合わせて使用して、インターネット上の一連のドキュメントを要求したり、移動したりできます。

ここ数年、これらの機能は基本的には変わっていませんが、AJAX の流行により背後で行われることが変化しました。背後で行われる AJAX 呼び出しにより、以前は一連のドキュメントだったものが、1 つのファイルになります。これは優れた機能で、パフォーマンス上のメリットやユーザー エクスペリエンスの向上は言うまでもなく、ほんの少し非同期にもなり、リッチなエクスペリエンスを実現できます。しかし、いくつか問題も生じます。たとえば、ユーザーが戻るボタンをクリックしたり、ページを最新の情報に更新したりするときに、ブラウザーがどのページを表示すべきか認識するように、URL を追跡し続けることがはるかに困難になります。

URL の重要性を誇張しすぎているとは思いません。URL は固定的なものです。ユーザーは URL をお気に入りに登録します。検索エンジンは URL にインデックスを付けます。企業は URL を売買します。変化を続ける Web 環境でこのような URL を管理できるでしょうか。

ハッシュ (#) とハッシュバング (#!) に話を移します。ブラウザーは URL フラグメント識別子としての # の後ろにあるものをすべて考慮しますが、サーバーに送信することはありません。ハッシュはもともとページ内にリンクするためのアンカー タグでしたが、AJAX 関連技術にも使用されるようになりました。ただし、AJAX の初期の検索エンジンは、ハッシュを含む URL にインデックスを付けませんでした。これを解決するのがハッシュバングです。これにより、Web アプリケーションはサーバーにページ要求を一度も返すことなく、URL の使用や変更が可能で、検索エンジンはこの URL にインデックスを付けることができます。twitter.com/#!replies のようにハッシュバングを含む URL は、Web アプリケーションの出現に伴って重要になってきました。

新しい History API

この API は実に大きな前進ですが、ブラウザーの公式サポートはまったくありませんでした。何か機能させるには、Web アプリケーションでスクリプトを使用するしかありません。新しい History API は Web アプリケーションで "状態管理" を可能にすることに重点を置いています。状態管理とは、セッション履歴を構成するドキュメントのシーケンスを追跡することです。その結果、セッション履歴を操作でき、状態データを保存することができます。つまり URL を適切に扱うことができます。この API についての調査を始める前に、World Wide Web コンソーシアム (W3C) が仕様として策定した実際のインターフェイス定義を見てみましょう (bit.ly/MU89iZ、英語))。

interface History {
  readonly attribute long length;
  readonly attribute any state;
  void go(optional long delta);
  void back();
  void forward();
  void pushState(any data, 
    DOMString title, 
    optional DOMString url);
  void replaceState(any data, 
    DOMString title, 
    optional DOMString url);
};

どう考えても、複雑な API ではありません。

どのようにして機能するかを理解するために、ドキュメント間を単純に移動することから始めましょう。実際に a.cshtml、b.cshtml、c.cshtml という 3 つのドキュメントがあり、これらすべてを移動するものとします。通常は、<a href="http://foo.com/a.cshmtl">a に移動</a> というようなアンカー タグを作成するだけです。ユーザーがこのアンカーをクリックすると、ブラウザーは通常のページとサーバーのライフサイクルを強制的に移動させられます。ユーザーがクリックして任意の Web サイトに移動すると、セッション履歴が作成されます。

ただし、この方法には 2 つの潜在的な問題があります。1 つは、ブラウザーはページ サイクル全体を強制的に移動させられ、サーバーさえ呼び出すことです。もう 1 つはその URL を表す物理ドキュメントが必要になることです。

AJAX は、ページの一部のみの要求を許可し、ページ全体を要求する回数を減らし、http://foo.com/#!a や http://foo.com/\#a のように URL を手動で更新できるようにすることで、この問題の一部を解決しています。History API を用いてこれと同様の結果を実現するには、保存する状態、ページのタイトル、および表示する URL を渡して、window.history.pushState(state, title, url) を呼び出します。ハッシュやハッシュバングを含む URL を使用する必要がありません。URL は http://foo.com/a のまま使用でき、"a" ドキュメントが物理的に存在しなくてもかまいません。

pushState を呼び出すことにより、ユーザーのセッションのセッション履歴を作成しています。ユーザーは閲覧したいページに移動でき、思ったとおり機能します。ユーザーが戻るボタンをクリックすると、想定どおり前の URL に戻ることができ、前にあった複数の URL は、ページが普通につながっているかのように存在し続けます。

また、ユーザーのセッション履歴を移動したり、見て回ったりできるしくみもあります。ユーザーが進むボタンや戻るボタンをクリックしたかのように、スタックを通じて前のページや後ろのページに動的に移動することができます。

実際の例

実際の例を使って具体的に説明しましょう。私は That Conference というテクノロジ カンファレンス (thatconference.com、英語) を主催しています。カンファレンスには多くの講演者がいますが、講演者ごとにページを作成するつもりはありません。実際に出演する講演者ごとにページを動的に作成する予定です。History API を使用すれば簡単です。

スクリプト中心の Web アプリケーションと同様に、データが必要です。さいわい、That Conference には講演者のデータを取得するために呼び出すことができる、シンプルな Representational State Transfer (REST) API があります (thatConference.com/api/person)。この呼び出しは JSON または XML のいずれかで、指定した年の講演者の配列を生成します。図 1 に生成した配列内の項目を示します。

図 1 講演者のプロフィール

<PersonViewModel>
  <FirstName>Scott</FirstName>
  <LastName>Hanselman</LastName>
  <Company>Microsoft</Company>
  <Bio>
    My name is Scott Hanselman. I work out of my home office for Microsoft as a Principal Program Manager, aiming to spread good information about developing software, usually on the Microsoft stack. Before this I was the Chief Architect at Corillian Corporation, now a part of Checkfree, for 6-plus years. I was also involved in a few Microsoft Developer things for many years like the MVP and RD programs and I'll speak about computers (and other passions) whenever someone will listen.
  </Bio>
  <Twitter>shanselman</Twitter>
  <WebSite>http://www.hanselman.com</WebSite>
  <Gravatar>/Images/People/2012Speakers/ScottHanselman.png</Gravatar>
</PersonViewModel>

表示する方法がなければデータがあっても役に立ちません。講演者ごとのページを動的に作成するために使用できる、単純なマークアップ テンプレートをセットアップする必要があります。このために、Knockout (knockoutjs.com、英語) というフレームワークを使用します。Knockout は、開発者がモデル ビュー ビューモデル (MVVM) パターンで宣言型のバインドを使用できるようにする JavaScript ライブラリです。History API には特に Knockout を使用しなくてもかまいませんが、ちょっと面白くなるので使用しています。

すべての講演者のページは同じなので、Knockout で単純なマークアップ テンプレートを定義するつもりです。スクリプト ブロックを作成し、後でどのようにして設定するかをフレームワークに通知する必要があります。

<script type="text/html" id="person-template">
  <div>
    <p>
      <strong>Name:</strong>
        <span data-bind="text: FirstName"></span>
        <span data-bind="text: LastName"></span>
    </p>
    <p>Company: <strong data-bind="text: Company"></strong></p>
    <p>Bio: <strong data-bind="text: Bio"></strong></p>
  </div>
</script>

次に、テンプレートを設定します。そのためには、ko.applyBindings(someData) を呼び出します。すると Knockout が applyBindings に渡したオブジェクトが何であっても適切に処理します。これで、講演者のオブジェクトを受け取り、マークアップにそのオブジェクトのデータを設定する基本メカニズムができました。

しかし、目標としていることはもう少し複雑です。本当に望んでいることは、ユーザーが一連のページをパラパラめくれるように、いわば講演者の書籍を作成することです。以下はページが最初に読み込まれるときに必要な作業です。

  1. 講演者を表す JSON 形式のデータを取得する
  2. 配列内の最初の項目を既定値として Knockout テンプレートにバインドする
  3. 適切な引数を渡して window.pushState を呼び出す

既に最初の 2 つの手順は説明したので、pushState について説明します。window.pushState を呼び出すことによって、ユーザーのセッション履歴に項目を作成していることになります。pushState を呼び出しには 3 つの項目を渡します。

  • State: この例では、Knockout テンプレートにバインドする配列項目です。
  • Title: ページのタイトルです。ここでは、講演者のフル ネームになります。
  • URL: ページの URL です。ここでは thatconference.com/speakers/speaker/SpeakerFullName のようになります。

このロジックをすべて次のように bind というメソッドにラップしています。

function bind (speakerID) {
  var speakerVM = new speakerViewModel(speakerID);
  var fullName = speakers[speakerID].FirstName 
    + speakers[speakerID].LastName
  window.history.pushState(speakerVM, 
    fullName, "/speakers/" + fullName); 
  ko.applyBindings(speakerVM);
}

ここで、講演者の書籍の講演者データ ページをめくる 2 つのボタンを追加します。

<button id="prevSpeaker">previous speaker</button>
<button id="nextSpeaker">next speaker</button>

もちろん、nextSpeaker ボタンと prevSpeaker ボタンに対応する 2 つのイベント ハンドラーが必要です。次に表示する講演者を追跡するために、ユーザーの操作を扱う単純なカウンターを作成します。カウンターの値は次のように bind メソッドに渡します。

var counter = 0;
$('#nextSpeaker').click( function () {
  counter = counter + 1;   
  bind(counter);
});
$('#prevSpeaker').click( function () {
  counter = counter - 1;
  window.history.back();
});

この時点で、なんらかの既定データを読み込んだページが用意されたので、[nextSpeaker] をクリックすると、講演者配列にある次の講演者の取得を続行します。ただし、[prevSpeaker] をクリックしても何も起こりません。さらに作業が必要です。

イベント

戻るボタン (またはスクリプト) が windows.history.back を呼び出すと、onpopstate イベントが発生します。これはユーザーのセッション履歴内を前に移動するしくみです。onpopstate イベントが発生すると、指定された state データを pushState に渡します。この例では、1 人の講演者を渡します。

ここで、この state データを取得し、バインドするよう Knockout に指示する必要があります。

window.onpopstate = function (event) {
  console.log('onpopstate event was fired');
  ko.applyBindings( event.state );
};

これで、想定どおり、セッション履歴を前後に移動できるようになります。ブラウザーの戻るボタンや用意した [prevSpeaker] ボタンのクリックに応じて、講演者が変わるのを確認できます。

今後について

ここでは、History API の表面をなぞったに過ぎません。ユーザーが講演者ページをお気に入りに登録して、後からそのサイトにアクセスする場合や、この問題に関連して、これらの講演者ページの 1 ページを作成している最中にユーザーが更新ボタンをクリックした場合にどう処理するのかは説明していません。

このようなシナリオには多くの方法があるため、明示的に説明しませんでしたが、サイトの構成方法や使用するテクノロジによって方法が変わります。# または #! の使用をサブスクライブする場合は、window.location.hash を呼び出して URL フラグメントを取得してから、サービスを呼び出してそのハッシュに適切なデータを取得し、取得したデータをマークアップにバインドするだけです。

今回の解決策では、ページ全体を動的に作成しますが、既存のページの一部分だけに History API を使用することもできます。このようにすると、ページの中核部分はサーバーを利用しますが、ページの一部は History API を使用するようになります。このような場合に使用する優れた詳しい例については bit.ly/vOlB2U (英語) を参照してください。

また、Web アプリケーションに機能検出を実装することも必要です。ユーザー エージェントのアクションを基にするのではなく、Modernizr (modernizr.com、英語) などのツールを利用してブラウザーの機能を検出することをお勧めします。ユーザーのブラウザーが特定の機能をサポートしていない場合、ポリフィル (不足している機能をブラウザーに実装するシム) を使用することができます。ポリフィルは、CSS などの機能にも実行できます。機能検出の詳細については、Brandon Satrom の記事「取り残されるブラウザーをなくす: HTML5 の導入戦略」(msdn.microsoft.com/magazine/hh394148)(2011年 9 月) を参照してください。

AJAX により、インターネット上での Web サイトの操作方法が変わり、Web 開発者は標準の Web サイトをリッチな Web アプリケーションに変えるクリエイティブな解決策を見つけました。ここで紹介した History API を使用すると、スクリプト中心の Web アプリケーションがブラウザーの中核となる基本機能を変更しないようにすることができます。

ここで紹介したすべては、Microsoft Web Matrix を使用する Windows 8 Release Preview で実行しました。すべてのコードについては on.csell.net/msdn-historylesson (英語) を参照してください。History API の詳細を知るための多くの優れたリソースについては on.csell.net/msdn-historylesson-linkstack (英語) を参照してください。

Clark Sell は、シカゴ郊外でマイクロソフトのシニア Web エバンジェリストとして活躍しています。彼のブログは csell.net (英語) で、ポッドキャストは DeveloperSmackdown.com (英語) で公開されており、Twitter は twitter.com/csell5 (英語) からアクセスできます。

この記事のレビューに協力してくれた技術スタッフの John Hrvatin、Mark Nichols、Tony Ross、および Brandon Satrom に心より感謝いたします。