はじめに
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)を見ているつもりでも、ブラウザ上ではこのアプリケーションの履歴スタックは次のようになっているのです。
そのため、ユーザーは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=1とpage=2)は、ユーザーが現在見ている検索ページを指しています。
RSH(Really Simple History)
AJAXのUI問題に注目し、AJAXベースのアプリケーションでブラウザのデフォルト動作を維持するための取り組みを行っているフレームワークがいくつかあります。その1つがRSH(Really Simple History)フレームワークです。このフレームワークは、DhtmlHistoryとHistoryStorageという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 検索アプリケーションのディレクトリ構造。サンプルの検索アプリケーションはこれらのフォルダを使用
図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()メソッドは、searchTxtとpageNumberという2つのパラメータをとります。前者はテキストボックスに入力された検索テキストを表し、後者は移動先となる検索ページを表します。performSearch()は非同期のリクエストを「doSearch.jsp」に送り、「doSearch.jsp」は検索結果を取得します。検索結果のダミーを図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()関数は、searchTxtとpageNumberの両方が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()メソッドには、検索を開始するという役目があります。このメソッドはパラメータとして受け取ったsearchTextとpageNmuberを使用して「doSearch.jsp」を呼び出し、検索結果を表示します。
これで検索アプリケーションが完成しました。http://yourdomain/searchMain.jspというURLをブラウズすれば、検索ページを表示することができます。
メインフォームが表示されたところで検索テキスト(「What is Dojo」など)を入力すると、同じページに検索結果が表示されます(図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アプリケーションの基礎として利用してください。
大企業のWebアプリケーション開発に携わって4年以上の経験を持つソフトウェアエンジニア。現在はHCL Technologies, Indiaの主任エンジニアとして勤務。