japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年7月4日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2008年2月15日 10:00

AJAXアプリケーションで標準的なブラウザナビゲーションを再現する

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!
  • この記事をokyuuへインポート

はじめに

 AJAXは、デスクトップアプリケーションと同じくらい対話性と応答性のよいリッチなWebアプリケーションを開発する手段として、多くの開発者に採用されてきました。AJAXでは、WebのUIを異なるセグメントに分割します。ユーザーはあるセグメントで操作を実行し、その操作が終わらないうちに他のセグメントで作業を開始することができます。

 しかし、AJAXには大きな欠点があります。戻る、進む、ブックマークといった標準的なブラウザ機能が無効になるのです。AJAXアプリケーションの開発者は、ユーザーをAJAXの欠点に無理やり順応させるのではなく、アプリケーションを従来のWebインタラクションスタイルに合わせ、次の機能を提供するようにしなければなりません。

  • [戻る]/[進む]ボタンが機能するようにして、エンドユーザーが直観的なやり方で履歴ページ間を移動できるようにする。
  • ユーザーがブックマークを作成できるようにする。
  • [更新]ボタンが予想どおりに機能するようにして、現在の状態が更新されるようにする(アプリケーションを再初期化したり、予想外のページや状態に再設定したりしない)。
  • エンドユーザーがブラウザの標準の検索機能でページを検索できるようにする。
  • 検索エンジンがAJAXアプリケーション内のページをインデックス化して、検索語までのディープリンクを作成できるようにする。

 典型的なAJAXアプリケーションは最初の3つの機能を実現していません。XMLHttpRequestオブジェクトやDOM更新などのAJAXテクノロジを使用する動的なWebアプリケーションは、ブラウザの履歴を正しく更新しません。なぜなら、バックグラウンドリクエストを使ってデータを取得すると、ページURLが変更されないからです。そのため、ユーザーが[戻る]ボタンをクリックすると、おそらくWebアプリケーションから彼方にジャンプして、保存された状態が失われることになるでしょう。[更新]ボタンをクリックすると、AJAXアプリケーションが再初期化され、現在のページの初期状態に戻ります(実際、アプリケーションが再起動されることが多いでしょう)。同じ理由で、ブックマークも機能しません。ブックマークを使ってAJAXのURLに戻っても、ユーザーがブックマークを付けた時点のページ状態にならないかもしれません。

 ユーザーの立場からすると、標準的なナビゲーション動作はとても重要です。ユーザーはWebアプリケーションで一定のインタラクションが当然起こるものと期待しますが、典型的なAJAXアプリケーションでは必ずしもそうならないのです。たとえば、ユーザーがブラウザのURLを使ってページにブックマークを付けた場合、そのページを作成するためにAJAXアプリケーションが作成した中間ステップ(バックグラウンドリクエスト)についての情報は失われます。また、ユーザーがブラウザの[戻る]ボタンをクリックすると、前のページが表示されますが、これは一般に前のページ状態ではありません。AJAXアプリケーションでは「ページ」と「ページ状態」の違いが重要になります。なぜなら、この2つはユーザーにとっては同じことでも、ブラウザにとってはまったく別物だからです。

 例として、AJAXベースの検索アプリケーションを考えてみましょう。最初のページには、ユーザーが検索条件を入力する検索フォームが含まれています。これをPage1とします。ユーザーが入力したフォームを送信すると、最初の数個の検索結果が含まれたページ(Page2)が表示されます。このページには、次の検索結果セットを含むページ(Page3)を見るためのリンクが表示されます(さらに後続のページがある場合は、同様に続きます)。アプリケーションがPage3のための次の検索結果セットを取得するためには、非同期呼び出しを送信し、取得したデータをPage2のDOM構造に入れるという処理を行います。このとき、ページ状態は変化しますが、ページのURLは変化せずPage2のままです。つまり、ユーザーは新しいページ(Page3)を見ているつもりでも、ブラウザ上ではこのアプリケーションの履歴スタックは次のようになっているのです。

Page2
Page1

 そのため、ユーザーはPage3で[戻る]ボタンをクリックしたときに、当然、最初の検索結果セット(元のPage2)が表示されるものと期待しますが、ブラウザは実直にPage1に戻ります。同様に、ユーザーがPage3を見ているときにページの更新を行うと、代わりにPage2が表示されます。このような直観に反した動作により、ユーザーが混乱し、アプリケーションが使いにくくなります。

解決策

 AJAXアプリケーションで作成される各ドキュメントをそれ自身のURLでアドレスできれば、[戻る]ボタンとブックマークの問題を解決できるでしょう。この検索アプリケーションが最初の結果ページと2番目のページを別々のURLで参照していることに注目してください。つまり、2つのURLがそれぞれ異なるリソースドキュメントを指しているわけです。しかし、実際にAJAXドキュメントを構成している要素は、開始時点のドキュメントと、ドキュメントのノードを追加、削除、変更する不定の数のDOM操作です。したがって、ある時点のAJAXドキュメントを再作成するには、まず開始時点のドキュメントのURLを取得したうえで、目的の時点までに行われたDOM変更を適用する必要があります。各AJAXリクエストが一意のURLを持っていれば、ずっと簡単にできるでしょう。

URLハッシュの入力

 URLハッシュとは、シャープ記号(#)の後に続くURL部分です。通常、URLハッシュは他のページではなく、同じページ内の特定の位置を指すので、長いページ内の特定の位置にジャンプするためのリンクで使われます。しかし、必ずしもそういう使い方をする必要はありません。URLハッシュを使って、DOM操作に関する情報をAJAXアプリケーション内に格納する、つまり各AJAXリクエストに一意のURLを与えることもできます。

 URLハッシュには、AJAX履歴を作成するうえで非常に有利な点が2つあります。その1つは、現在のURL内のハッシュを変更してもページの再ロードが起こらないことです。そのため、ページを変更せずに一意のURLを作成することができます。もう1つの利点は、ブラウザがもともとURLハッシュをページ履歴の一部として扱っていることです。つまり、URLハッシュに移動すると、ブラウザの履歴リストにエントリが追加されます。現在のURLのハッシュマークの後ろに明確なエントリを追加すれば、AJAXアプリケーションで標準的な前進/後退動作とブックマーク機能を再現するのに役立つ履歴トレールを作成できます。たとえば、次のようなURLハッシュを使って、検索アプリケーションの2つの結果ページを指すことができます。

http://searchserver/search.jsp?searchText=television#page=1
http://searchserver/search.jsp?searchText=television#page=2

 これらのURLハッシュ(page=1page=2)は、ユーザーが現在見ている検索ページを指しています。

RSH(Really Simple History)

 AJAXのUI問題に注目し、AJAXベースのアプリケーションでブラウザのデフォルト動作を維持するための取り組みを行っているフレームワークがいくつかあります。その1つがRSH(Really Simple History)フレームワークです。このフレームワークは、DhtmlHistoryHistoryStorageという2つのオープンソースJavaScriptクラスから成っています。この2つのクラスを使用することにより、AJAXアプリケーションでブックマークと[戻る]/[進む]ボタンをサポートすることが可能になります。

 DhtmlHistoryクラスはAJAXアプリケーションの履歴の抽象化を実現します。ブラウザに履歴イベントを追加するには、AJAXページでadd()メソッドを呼び出して新しい位置と関連履歴データを指定します。DhtmlHistoryクラスは、#new-locationなどのアンカーハッシュを使ってブラウザの現在のURLを更新し、この新しいURLに履歴データを関連付けます。AJAXアプリケーションは自分自身を履歴リスナとして登録します。ユーザーが[戻る]ボタンと[進む]ボタンで移動を行うと、ブラウザはadd()呼び出しで保存されたブラウザの新しい位置と履歴データを提供する履歴イベントを作動させます。

 また、HistoryStorageクラスを使用すると、任意の量の履歴データを保存することができます。通常のWebページでは、ユーザーが新しいWebサイトに移動すると、ブラウザは元のWebページのアプリケーションとJavaScriptの状態をアンロードして消し去ります。ユーザーが後から[戻る]ボタンでそのページに戻った場合、そのデータはすっかり失われています。HistoryStorageクラスはこの問題を解決するために、put()get()hasKey()といった単純なハッシュテーブルメソッドを含むAPIを公開しています。開発者はこれらのメソッドを使用して、ユーザーがWebページから去った後も任意の量のデータを保存することができます。ユーザーが後から[戻る]ボタンで戻った場合、ページ内のコードはHistoryStorageクラスを通じて保存されたデータにアクセスできます。内部的な話になりますが、このデータストレージは隠されたフォームフィールドによって実現されており、ユーザーがページから去った後もブラウザがフォームフィールド値を自動的に保存するという点を利用しています。

Dojo

 AJAXのナビゲーション問題のもう1つの解決策となるのがDojoです。DojoはWebアプリケーションが[戻る]ボタンと[進む]ボタンのクリックを捕捉し、ブラウザのロケーションフィールドにブックマーク用の一意のURLを設定できるようにします。

 DojoはJavaScriptで書かれたオープンソースのDHTMLツールキットで、Webページ(またはJavaScriptをサポートする他の環境)に動的な機能を組み込むのに役立ちます。Dojoのコンポーネントを使用すると、Webサイトの使いやすさと応答性と機能性を高めることができます。Dojoでは、複雑なユーザーインターフェイスを容易に構築したり、対話的なウィジェットをすばやくプロトタイプ化したり、トランジションをアニメーション化したりできます。Dojoの低レベルのAPIと互換性レイヤは、移植可能なJavaScriptの記述やスクリプトの単純化に貢献します。さらに、そのイベントシステムや入出力API、汎用言語拡張は、強力なプログラミング環境の基盤となります。

 dojo.undo.browserモジュールはブラウザの履歴へのアクセスを提供し、ユーザーがAJAXアプリケーションから出なくても[戻る]ボタンと[進む]ボタンをクリックできるようにします。このツールキットは[戻る]イベントと[進む]イベントが発生したときにコールバックメッセージを発行して、開発者にWebアプリケーションを適切に更新する機会を与えます。Dojoは隠されたIFRAMEを使ったり、ページURLのハッシュ部分に一意の値を追加したりすることで、ブラウザの履歴を生成します。

 URLハッシュ形式に含まれるアプリケーション状態識別子を変更してもページの更新は行われないので、アプリケーションの状態を保持するにはURLハッシュを使用するのが理想的です。Dojoが生成する一意のハッシュ値はもともとブックマークをサポートしていますが、アプリケーション状態識別子としてもっと意味のある値を指定すれば、ブックマークの可読性を高めることができます。Dojoの状態オブジェクトはページの状態を表します。この状態オブジェクトは、ユーザーが[戻る]ボタンと[進む]ボタンをクリックしたときにコールバックを取得します。状態オブジェクトはdojo.undo.browserに直接登録するだけでなく、イベントをバインドするdojo.io.bind()に渡すこともできます。

Dojoの設定

 dojo.undo.browserを使用するには次のようにします。

  • djConfig内でpreventBackButtonFixプロパティをfalseとして定義します。このプロパティにより、Dojoが隠されたIFRAMEをdocument.write()コマンドで現在のページに追加できるようになります。これを行わないと、dojo.undo.browserは正しく機能しません。
  • 適切なrequireステートメント(dojo.require("dojo.undo.browser");)を追加します。
  • dojo.undo.browser.setInitialState(state);を呼び出すことにより、ページの初期状態を登録します。stateの部分には、ユーザーが[戻る]ボタンをクリックしたときに通知される状態オブジェクトを指定します。これでWebアプリケーションの開始まで戻ります(ユーザーが[戻る]ボタンをもう一度クリックすると、ブラウザはアプリケーションに先行するページ(もしあれば)に移動します)。

 状態オブジェクトでは次の機能を定義する必要があります。

  • [戻る]通知の取得: back()backButton()、またはhandle(type)typeには文字列"back"を指定)
  • [進む]通知の取得: forward()forwardButton()、またはhandle(type)typeには文字列"forward"を指定)

 以下は非常に単純な状態オブジェクトの例です。

var state = {   
   back: function() { 
      alert("Back was clicked!"); 
   },   
   forward: function() { 
      alert("Forward was clicked!"); 
   }
};

 ユーザーアクションの結果を表す状態オブジェクトを登録するには、次の呼び出しを使用します。

dojo.undo.browser.addToHistory(state);

 あるいは、dojo.io.bind()を使用する場合で、かつ状態オブジェクトにback()関数、backButton()関数、またはchangeUrlプロパティが含まれているときは、dojo.io.bind()がdojo.undo.browserの呼び出しを処理します(これはXMLHTTPTransportとScriptSrcTransportのみを処理します)。

 ブラウザのアドレスバー内のURLを変更するには、状態オブジェクトにchangeUrlプロパティを含めます。このプロパティをtrueに設定すると、dojo.undo.browserはフラグメント識別子に対して一意の値を生成します。それ以外の値(未定義、null、ゼロ、空の文字列を除く)に設定すると、その値がフラグメント識別子として使われます。つまり、開発者はDojoにハッシュ値を選択させるか、カスタムハッシュ値を設定することができます。どちらにしても、この機能によって、コードが現在のページ状態を再構築できるようなやり方で、ユーザーがページにブックマークを付けることが可能になります。

検索アプリケーション

 以下では、前述した検索アプリケーションの問題にDojoの履歴機能を適用する方法を実例で示します。図1は、検索アプリケーションのディレクトリ構造を示しています。

図1 検索アプリケーションのディレクトリ構造。サンプルの検索アプリケーションはこれらのフォルダを使用
図1 検索アプリケーションのディレクトリ構造。サンプルの検索アプリケーションはこれらのフォルダを使用
図2 単純な検索フォーム。ユーザーは検索テキストを入力してから[Search]ボタンをクリックする。これにより、サーバーにバックグラウンドリクエストが送られる
図2 単純な検索フォーム。ユーザーは検索テキストを入力してから[Search]ボタンをクリックする。これにより、サーバーにバックグラウンドリクエストが送られる

 ユーザーが検索をトリガするメインページ(searchMain.jsp、図2を参照)は、アプリケーションのルートフォルダに置かれています。「dojo」フォルダには、「dojo.js」などの標準Dojoファイルが含まれています。「script」フォルダには、検索アプリケーション用の特別なJavaScriptファイルとして、アプリケーション状態実装を定義する「HistoryTracker.js」と、body onloadハンドラ関数や検索を開始する関数など、さまざまなJavaScriptユーティリティ関数を定義する「dojoUtility.js」が含まれています。

著者注
 このサンプルアプリケーションではJSPを使用していますが、この例はどんなサーバーサイドテクノロジ(PHPやASPなど)を使用したとしても同様にうまく働きます。

 図2のフォームを表すHTMLの<form>タグは次のように記述されています。

<form name="searchForm" action="#">
   <input type="text" name="searchTextElement"></input>
   <input type="button" name="Search" value="Search" 
      onclick="performSearch(
      this.form.searchTextElement.value);"></input>
</form>

 ここで、ボタンを表す<input>タグのtype属性に"submit"ではなく"button"を使用していることに注意してください。これは自動フォーム送信を避けるためです。今回のサンプルでは、非同期のバックグラウンドリクエストを送信して、検索結果を取得します。したがって、ボタンのonclickイベントでperformSearch()メソッドを呼び出し、このメソッドから検索操作をトリガします。

 以下のperformSearch()関数コードは独立した「dojoUtility.js」ファイルに入っています。

function performSearch(searchTxt, pageNumber) {
   var bindUrl = "/dojoapp/doSearch.jsp";
   if(searchTxt) {
      bindUrl += "?searchTxt=" + searchTxt ;
      if(pageNumber) {
         bindUrl += "&pageNumber=" + pageNumber;
      }
      dojo.io.bind({
         url: bindUrl,
         load: function(type, data, evt){
            dojo.undo.browser.addToHistory(
               new HistoryTracker(data, searchTxt, 
               pageNumber, "searchContent"));
            dojo.byId("searchContent").innerHTML = data;
         }
      });
   }
}

 performSearch()メソッドは、searchTxtpageNumberという2つのパラメータをとります。前者はテキストボックスに入力された検索テキストを表し、後者は移動先となる検索ページを表します。performSearch()は非同期のリクエストを「doSearch.jsp」に送り、「doSearch.jsp」は検索結果を取得します。検索結果のダミーを図3に示します。

図3 検索結果のダミー。ページの下部に検索結果が表示される。
図3 検索結果のダミー。ページの下部に検索結果が表示される。

 以下は検索結果を生成するコードです(doSearch.js)。

You have searched for: 
   <%= request.getParameter("searchTxt") %>
<br/>

Showing page number: 
   <%= (request.getParameter(
   "pageNumber") == null)  ? 
   "1" : request.getParameter(
   "pageNumber") %>
<br/>

<a href="javascript:performSearch(
   '<%= request.getParameter("searchTxt") %>', 
   '<%= (request.getParameter("pageNumber") == null)  ?  "2" : 
   (Integer.parseInt(request.getParameter(
   "pageNumber") ) + 1) %>');">View next set of results</a>

 これはページ番号と検索テキストのみを表示するダミーページです。このページには、次の検索結果セットを取得するためのアンカータグが含まれており、このタグのhref属性の値としてjavascript:performSearch()関数を指定しています。この関数はパラメータとして検索テキストと次のページ番号をとります。

 performSearch()関数はHistoryTrackerオブジェクトを使用します。アプリケーション状態変化の登録はdojo.undo.browser.addToHistory()を呼び出すことによって行います。ただし、そのためにはアプリケーションの初期状態を登録しておく必要があります。通常、これはbody onloadイベントの中で行われます。

dojo.addOnLoad(handleBodyLoad);

 handleBodyLoad()関数の定義は次のようになっています。

function handleBodyLoad() {
   var state = new HistoryTracker(null, null, null, "searchContent");
   dojo.undo.browser.setInitialState(state);
   handleUrlHash();
}

 handleBodyLoad()関数は、searchTxtpageNumberの両方がnull値となる状態オブジェクトを作成します。その後、この状態オブジェクトをdojo.undo.browser.setInitialState()に渡すことで、アプリケーションの初期状態を登録します。そして最後にhandleUrlHash()を呼び出します。handleUrlHash()はURLハッシュを解析し、ハッシュの値に応じてアプリケーションの状態を復元します。

function  handleUrlHash() {
   var urlHash = location.hash.substring(1);
   var searchText, pageNumber;

   if(urlHash && urlHash != '') {
      var hashParams = urlHash.split(";");

      for (i = 0; i < hashParams.length; i++) {
         var temp = hashParams[i].split("=");
         if(temp && temp.length > 0) {
            switch(temp[0]) {
               case 'searchTxt': 
                  searchText = temp[1];
                  break;
               case 'pageNumber': 
                  pageNumber = temp[1];
                  break;
            }
         }
      }
   }
   if(searchText) {
      if(pageNumber) {
         performSearch(searchText, pageNumber);
      } 
      else {
         performSearch(searchText, 1);
      }
   }
}

 handleUrlHash()メソッドはURLハッシュを解析して検索テキストとページ番号を取得し、解釈した検索テキストとページ番号を使ってperformSearch()を呼び出します。前記のperformSearch()メソッドには、検索を開始するという役目があります。このメソッドはパラメータとして受け取ったsearchTextpageNmuberを使用して「doSearch.jsp」を呼び出し、検索結果を表示します。

 これで検索アプリケーションが完成しました。http://yourdomain/searchMain.jspというURLをブラウズすれば、検索ページを表示することができます。

 メインフォームが表示されたところで検索テキスト(「What is Dojo」など)を入力すると、同じページに検索結果が表示されます(図4を参照)。

図4 検索結果。検索結果は検索フォームと同じページに表示される
図4 検索結果。検索結果は検索フォームと同じページに表示される

 ページは変化しませんが、次のように、元のURLの末尾にURLハッシュが追加されていることがわかるでしょう。

http://localhost:8080/dojoapp/searchMain.jsp
#searchTxt=What%20is%20Dojo;pageNumber=1

 このURLハッシュには、searchTxtとpageNumberの値がセミコロンで区切られて含まれています。検索結果の「View next set of results」リンクをクリックすると、2番目の検索結果ページが表示されます。

 ご覧のように、DojoはAJAXアプリケーションにブックマークおよび[戻る]ボタンと[進む]ボタンを考慮させることができます。サンプルアプリケーションで示したように、各ページに明確なURLハッシュを割り当てることにより、ブラウザの履歴が更新されます。このテクニックを使用すると[戻る]ボタンと[進む]ボタンが正しく機能するので、ユーザーは任意のページに簡単にブックマークを付けることができます。本稿のダウンロードサンプルにはサンプルアプリケーションのコードが収録されており、すぐに使用できる状態になっています。ここで紹介したアイデアを、読者自身のナビゲーション対応AJAXアプリケーションの基礎として利用してください。

著者紹介

Gautam Kumar Singh(Gautam Kumar Singh)
大企業のWebアプリケーション開発に携わって4年以上の経験を持つソフトウェアエンジニア。現在はHCL Technologies, Indiaの主任エンジニアとして勤務。
このエントリーを含むはてなブックマーク この記事をクリップ!
BuzzurlにブックマークBuzzurlにブックマーク Yahoo!ブックマークに登録
この記事をokyuuへインポート
最新トップニュース
データメーション
【データメーション】
中国が「Green Dam」フィルタ規制を撤回(7月1日)
Graphic Design Forum
【Graphic Design Forum】
Chris Dickman(6月25日)
プライバシー ジャパン・インターネットコム版
【プライバシー ジャパン・インターネットコム版】
グーグル・ストリートビューの問題について総務省の見解(6月23日)
エンジニアの独り言
【エンジニアの独り言】
システムを「使う」時代のエンジニアに求められるもの(6月2日)
最新ハイテク講座
最新ハイテク講座
電気は家庭でつくる時代へ!燃料電池「エネファーム」(7月3日)
アクセス解析で見るWebマーケティング
アクセス解析で見るWebマーケティング
決定力を探るアクセス解析(7月3日)
百式のネットビジネス研究
百式のネットビジネス研究
ファーストフードを高級っぽく盛り付けて紹介している「Fancy Fast Food」(7月3日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(7月2日)
成約率、反応率を上げる Web 文章術
成約率、反応率を上げる Web 文章術
言葉がダイレクトにキャッシュを生む(7月2日)
不況時代の Web ビジネス最適化講座
不況時代の Web ビジネス最適化講座
アクセス解析エキスパートここだけの話、Web コンシェルジュの“勉強法”こっそり教えます(7月2日)
「Webからの脅威」―その傾向と最新対策
「Webからの脅威」―その傾向と最新対策
不正プログラムの分類(7月1日)
DevX
DevX
JavaScriptとDOMによる動的なWebページの作成(6月30日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
今のままで大丈夫?3匹の子ブタ的キャリア危険度診断(6月30日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
Web サイトは「無駄な穴のたくさん開いたじょうご」〜サイト成果向上の基本的な考え方(6月30日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/