レベル: 中級 Mike Brittain (mike@mikebrittain.com), Director of Technology, ID Society
2006年 6月 06日 Ajax(Asynchronous JavaScript and XML)ドリブンのWebサイトの持つ大きな問題は、「戻る」ボタンがないことです。この「Ajaxを利用してPHPを開発する」(US)シリーズの第1回で作ったAjaxフォト・ギャラリー用の履歴スタックを、JavaScriptを使って作りましょう。この履歴スタックは、Webブラウザーに見られる履歴ユーティリティーを可能な限り真似たもので、アプリケーションに対して「戻る」「進む」「更新」ボタンの機能を提供します。
はじめに
第1回では、SajaxとPHP、そしてJavaScriptを使って基本的なフォト・ギャラリーを開発する方法について説明しました。このアプリケーション用の履歴スタックを構築するために、ここではクライアントサイド技術を使用して、第1回のコードの中に直接統合することにします。この記事では、JavaScriptとブラウザーのクッキーを理解していることを前提にしています。
ブラウザーの中に状態を保存する
皆さんがWebサーフィンをする場合には、クリックしながらページからページに、サイトからサイトに移って行きます。皆さんがそれを行っている間、Webブラウザーは皆さんが見たページの履歴をせっせと収集し、皆さんが出発したところに戻れるように、デジタル的な足跡を作成します。「戻る(Back)」ボタンは、最後のアクションを行う前にいたところに戻る、という意味で、Webに対する「取り消し(Undo)」ボタンと考えることができます。
Webはページ単位のメディアです。ブラウザーのツールバー上にある「戻る」ボタンと「進む」ボタンは、ブラウザーをページからページへと移動させます。MacromediaのFlashが爆発的な流行になると、開発者やユーザー達は、それまでの考え方がRIA(RichInternet Application)によって破壊されたことに気付きました。皆さんは幾つかのサイトをクリックして回り、今度はFlashベースのWebサイトに入って、数分間そこをクリックして回ります。そして「戻る」ボタンを1回クリックすると、お楽しみは終わりです。Flashサイト内で1ステップ前に戻るのではなく、自分がどこにいるのか、完全に分からなくなるのです。
RIAの別の形式である、Ajaxを多用したWebサイトでも同じことが言えます。1つのページ上で大量の対話動作を行わせようとするサイトは、「戻る」ボタンの問題が発生しがちです(あるいは、他の履歴ボタンでも同様です)。「進む」ボタンと「更新」ボタンにも、「戻る」ボタンと同じ問題が発生します。
Webブラウザーに組み込まれた内部的な履歴機構は避けることができません。セキュリティー上の理由から、開発者がブラウザーの履歴ボタンや、履歴に関連したボタンをいじることはできません。また、使いやすさの問題もあります。ユーザーが「戻る」ボタンを押したら、突然おかしな警告が表示されたり、新しいWebサイトに行ってしまったりしたら、ユーザーは混乱してしまいます。
履歴スタックを構築する
ブラウザーの履歴をいじることはできませんが、自分がRIAの中で使うために専用の履歴を作ることはできます。もちろん、この履歴はブラウザーの中にある標準のナビゲーション・ツールとは多少切り離されています。しかし先ほど指摘した通り、表現力豊かなアプリケーションは、ページからページに移るという標準的な原則とは多少切り離されているのです。
ここでは、アプリケーション中のイベント履歴を管理するためのスタックを作ります。つまり、リストを保存し、そのリストに次々と要素を追加するのです。スタックは、『LIFO(LastIn, First Out)』でアクセスされるデータを保存するために使われます。履歴をたどる際にはスタックの先頭からデータを引き出すわけではありませんが、このモデルは私達が必要とするものに充分似ています。JavaScriptでは、スタックは配列の中で管理されます。
スタックを使った作業を行うに当たって、スタックの中での現在の位置を示すポインターが必要です。アプリケーションの中でクリックを繰り返すと、新しいイベントがスタックの先頭にプッシュされ、ポインターは追加された最後の要素を指します。アプリケーションの「戻る」ボタンや「進む」ボタンをクリックすると、スタックには新しいイベントは追加されませんが、スタック内でポインターが移動します。
「戻る」ボタンを使った時に、ブラウザーの履歴の中で何が起こるかを考えてみてください。ブラウザーは最後に見たページに戻り、それまで無効になっていた「進む」ボタンが突然点灯します。新しいページに移ると、「進む」ボタンは再び無効になります。(ブラウザー履歴にある)それまで最新であった要素はスタックからポップされ、新しいイベントが最上部にプッシュされます。この振る舞いを、これから作る履歴スタックの中で複製します。
私達のゴールは、「戻る」、「進む」、「更新」という、実用的な履歴ボタン・セットを作ることです。これを図1に示します。
図1. 履歴ボタン、「戻る」「進む」「更新」を左に、それらが無効な状態を右に示す
再利用性を設計する
JavaScriptは、オブジェクトやクラスの作成に非常に緩い方法を使っていますが、それでもコードの中に再利用性を持たせることは可能です。まず、履歴スタックに必要な機能の概要を考えましょう。次に、このスタックをJavaScriptでモデル化します。そして履歴スタックとフォト・ギャラリー・アプリケーションを統合する前に、機能をテストするための単純なページを作ります。このページによって、この開発における2つの側面を確認することができます。第1に、テスト・ページを作ることによって、このクラスのコア機能の開発とテストのみに集中することができます。また、テスト用に別のページを作ることによって、履歴スタックの機能をフォト・ギャラリーの機能に直接混ぜ込んでしまうのを防ぐことができ、再利用性を高めることができます。
クッキーをキャッシュする
アプリケーションの履歴は、ブラウザー・セッションの期間中ずっと存在するようにします。この履歴スタック・オブジェクトは、ユーザーがフォト・ギャラリーのページ上にとどまっている間のみ存在します。このクラスは、履歴スタックが変化する度に、履歴スタック全体をブラウザーのクッキーにコピーします。もしユーザーがそのページを去り、後で同じブラウザー・セッションに戻った場合には、そのユーザーは、その人がアプリケーションを去った時のポイントと同じポイントに戻されます。
クラスを書く
では早速、履歴スタックに保存すべきデータ、つまり『プロパティー』について説明しましょう。スタック(『配列』)とポインターについては既に説明しました。『stack_limit』プロパティーを使うと、データが多すぎてクッキーがオーバーフローするのを防ぐことができます(リスト1)。現実的には、最も古いイベントを捨てる前に、40から50のイベントを保存したいものです。このテストでは、この値を15にします。
リスト1. 履歴スタックに対するコンストラクター(クラスのプロパティーを含む)
function HistoryStack ()
{
this.stack = new Array();
this.current = -1;
this.stack_limit = 15;
} |
こうした3つのプロパティーと共に、このクラスには、履歴スタックに要素を追加し、アイテムを取得し、スタック・データをブラウザー・クッキーに保存するためのメソッドが幾つかあります。まず、addResource()メソッドを見てみましょう。このメソッドは、履歴スタックの最後にアイテムをプッシュします(リスト2)。もしスタックがstack_limitよりも大きくなったら、最も古いエントリーがシフトされてスタックの前面から押し出されます。
リスト2. addResource() メソッドで履歴スタックの最後にリソースを追加する
HistoryStack.prototype.addResource = function(resource)
{
if (this.stack.length > 0) {
this.stack = this.stack.slice(0, this.current + 1);
}
this.stack.push(resource);
while (this.stack.length > this.stack_limit) {
this.stack.shift();
}
this.current = this.stack.length - 1;
this.save();
}; |
履歴スタック・クラスに追加される、次の3つのメソッドは、このクラスから情報を取得するために使われます(リスト3)。getCurrent()メソッドは、スタック・ポンターが現在指しているアイテムを返します。これは、スタックをナビゲートする場合に便利です。hasPrev()メソッドとhasNext() メソッドは、現在のアイテムの前または後ろに要素があるかどうかを示すブール値、または、スタックの最初または最後に達したかどうかを示すブール値を返します。これらは単純なメソッドですが、「戻る」ボタンや「進む」ボタンの状態を判断する際に便利です。
リスト3. 履歴スタック・クラスのメソッドを定義する
HistoryStack.prototype.addResource = function(resource)
HistoryStack.prototype.getCurrent = function ()
{
return this.stack[this.current];
};
HistoryStack.prototype.hasPrev = function()
{
return (this.current > 0);
};
HistoryStack.prototype.hasNext = function()
{
return (this.current < this.stack.length - 1
& this.current > -1);
}; |
これで履歴オブジェクトにアイテムを追加することができ、また、自分がスタックのどこにいるかが分かるようになります。しかし、スタックの中をナビゲートする方法が何もありません。リスト4で定義されるgo()メソッドを使うと、スタックの中を戻ったり進んだりすることができます。プラスやマイナスのインクリメントを渡すと、スタックの中を前に進んだり後ろに戻ったりすることができます。これは、JavaScriptに組み込みのlocation.go()メソッドに似ています。ここでは組み込みの機能を真似ようとしているので、既にあるものを元にメソッドをモデル化することにしましょう。
都合がよいことに、このメソッドの上に再ロード機能を載せてしまうことができます。このメソッドにプラスやマイナスの引数を渡すと、スタックの中をナビゲートすることができます。ゼロを渡すと、現在のページの再ロードが開始されます。
リスト4. 履歴スタックのためのgo() メソッド
HistoryStack.prototype.go = function(increment)
{
// Go back...
if (increment < 0) {
this.current = Math.max(0, this.current + increment);
// Go forward...
} else if (increment > 0) {
this.current = Math.min(this.stack.length - 1,
this.current + increment);
// Reload...
} else {
location.reload();
}
this.save();
}; |
ここまで、この新しいクラスで幾つかのことをしましたが、すべてのHistoryStackオブジェクトが現在の文書の中にある限り、どれも問題なく動作するはずです。ページが再ロードされた場合にデータが失われる問題については既に説明したので、今度は問題を解決することにしましょう。リスト5では、ブラウザー・クッキーの中のデータを設定し、またそのデータにアクセスするためのメソッドを追加しています。それぞれのクッキーに対して、名前と値の対の設定のみを処理します。ブラウザー・セッションよりも長くクッキーを保存したくはないので、有効期限の設定は気にする必要がありません。また、この例では単純にするために、他のパラメーター(secureやdomain、pathなど)は処理しません。
注意: このクラスを使って、クッキーに対してもっと高度なことをしたい場合には、全く別のクッキー管理クラスを使った方が賢明かも知れません。クッキーの設定と読み取りは、履歴スタックの作成という中心的な話題からは少し外れています。メソッドやプロパティーへのアクセスに関する考え方をJavaScriptから借りるとすると、それらはクラスに対してプライベートにすべきでしょう。
リスト5. ブラウザー・クッキーに値を設定し、値にアクセスするメソッド
HistoryStack.prototype.setCookie = function(name, value)
{
var cookie_str = name + "=" + escape(value);
document.cookie = cookie_str;
};
HistoryStack.prototype.getCookie = function(name)
{
if (!name) return '';
var raw_cookies, tmp, i;
var cookies = new Array();
raw_cookies = document.cookie.split('; ');
for (i=0; i < raw_cookies.length; i++) {
tmp = raw_cookies[i].split('=');
cookies[tmp[0]] = unescape(tmp[1]);
}
if (cookies[name] != null) {
return cookies[name];
} else {
return '';
}
}; |
任意のクッキーを管理できるメソッドを定義したら、履歴スタックの読み書きという特定なタスクを処理するクラスを、さらに2つ書きます。save()メソッドは、スタックをストリングに変換してクッキーに保存します。またload()は、そのストリングを構文解析して配列に戻します(履歴スタックの中のアイテムをいじるには、この配列を使います)。これをリスト6に示します。
リスト6. save() メソッドとload() メソッド
HistoryStack.prototype.save = function()
{
this.setCookie('CHStack', this.stack.toString());
this.setCookie('CHCurrent', this.current);
};
HistoryStack.prototype.load = function()
{
var tmp_stack = this.getCookie('CHStack');
if (tmp_stack != '') {
this.stack = tmp_stack.split(',');
}
var tmp_current = parseInt(this.getCookie('CHCurrent'));
if (tmp_current >= -1) {
this.current = tmp_current;
}
}; |
クラスをテストする
できあがったクラスを、単純なHTMLページとJavaScriptでテストします。このテストでは、履歴ボタンが最上部に表示され、アクティブなボタンだけがハイライトされてクリックできるようになっています。このページは、テスト用に高度なアプリケーションを作るようなことはせず、リンクがクリックされる度に、単純に乱数を発生します。これらの数字は、履歴スタックに割り当てるイベントに相当します。スタックはこのページ上に表示され、現在のポインターが指すアイテムは、太字でハイライトされます。
リスト7. 履歴スタックをテストするための単純なHTMLページ
<html>
<head>
<title></title>
</head>
<body>
<div id="historybuttons"></div>
<div>
<a href="#" onclick="do_add(); return false;">Add Random
Resource</a>
</div>
<div id="output" style="margin-top:40px;"></div>
</body>
</html> |
このHTMLページのヘッドに、リスト8に示すJavaScriptコードを追加する必要があります。このコードは、最初に新しい履歴スタック・オブジェクトをインスタンス化し、そしてブラウザーのクッキーに保存されていた既存の値をロードします。
4つのdo_*() 関数が定義されていますが、これらはイベント・ハンドラーであり、「戻る」「進む」「更新」ボタンに対するリンクと、リスト7に示すAddRandom Resourceに対するリンクに追加されます。
display() 関数は、履歴オブジェクトの現在の状態を調べ、履歴ボタン用のHTMLを生成します。また履歴の中に保存されているアイテムのリストも生成します。
リスト8. 履歴スタック・クラスとテスト・ページの統合に使われるJavaScript
<script type="text/javascript" src="history.js"></script>
<script type="text/javascript">
var myHistory = new HistoryStack();
myHistory.load();
function do_add()
{
var num = Math.round(Math.random() * 1000);
myHistory.addResource(num);
display();
return false;
}
function do_back()
{
myHistory.go(-1);
display();
}
function do_forward()
{
myHistory.go(1);
display();
}
function do_reload()
{
myHistory.go(0);
}
function display()
{
// Display history buttons
var str = '';
if (myHistory.hasPrev()) {
str += '<a href="#" onclick="do_back(); return false;">'
+ '<img src="icons/back_on.gif" alt="Back"
/></a> ';
} else {
str += '<img src="icons/back_off.gif" alt=" /> ';
}
if (myHistory.hasNext()) {
str += '<a href="#" onclick="do_forward(); return
false;">'
+ '<img src="icons/forward_on.gif" alt="Forward" />'
+ '</a> ';
} else {
str += '<img src="icons/forward_off.gif" alt=" /> ';
}
str += '<a href="#" onclick="do_reload(); return false;">'
+ '<img src="icons/reload.gif" alt="Reload"
/></a>';
document.getElementById("historybuttons").innerHTML = str;
// Display the current history stack, highlighting the current
// position.
var str = '<div>History:</div>';
for (i=0; i < myHistory.stack.length; i++) {
if (i == myHistory.current) {
str += '<div><b>' + myHistory.stack[i] +
'</b></div>';
} else {
str += '<div>' + myHistory.stack[i] + '</div>';
}
}
document.getElementById("output").innerHTML = str;
}
window.onload = function () {
display();
};
</script> |
このテスト・ページを実行すると、履歴ボタンが履歴スタックの状態に反応する様子を見ることができます(図2)。例えば、このページが最初にロードされた時には、履歴ボタンはどちらもグレー・アウトされています。スタックに幾つかリソースが追加されると、「戻る」ボタンがアクティブになります。クリックしてスタックを後ろに戻ると、「進む」ボタンが点灯します。何度かクリックして後ろに戻ってから「Add」をクリックすると、スタックは短縮され、新しい『イベント』が、短縮されたスタックの最上部にプッシュされます。
図2. 履歴スタック用のテスト・ページ
クラスをテストしたので、今度は本番です。
履歴オブジェクトとフォト・ギャラリーを統合する
第1回の記事が終わったところから続けることにし、フォト・ギャラリー・ページの中に、履歴スタックへのコールを直接追加します。PHPファイルには触れる必要はありません。
まず、履歴ボタンを置くためのdivタグを追加します。このタグはリスト7の中にありました。
<div id="historybuttons"></div>
|
履歴スタック・コードは、.jsファイルに保存する必要があります。このファイルはフォト・ギャラリー・ページにリンクされます。
<script type="text/javascript" src="history.js"></script>
|
履歴スタック・オブジェクトはインスタンス化する必要があり、またキャッシュからロードする必要があります。このオブジェクトをフォト・ギャラリー・ページ上にある既存のスクリプト・タグの上に追加します。
var myHistory = new HistoryStack();
myHistory.load();
|
この履歴スタック用のテスト・アプリケーションでは、乱数をイベントとして保存しただけでした。履歴には何でも保存できるのですが、誰かがアプリケーションの「戻る」ボタンをクリックした時に、履歴の中に何があるのか分かるようになっている必要があります。アプリケーションの中では、2つのアクションだけがx_get_table()関数とx_get_image() 関数に関係しています。ですから、各テーブルのリンクに対して、tableという名前にstart値とstep値を付けたものをイベント識別子として保存することができます(例えばtable-10-5など)。同様に画像に対しては、表示すべき画像のindexを付けてimage名を保存することができます(例えばimage-20など)。
第1回では、フォト・ギャラリー・アプリケーションの各リンクが、get_table_link()とget_image_link()という2つの関数のどちらかによって生成されるのを見てきました。これらの関数を編集して、Sajax関数を呼ぶ直前に履歴スタックへのコールを含めるようにします。リスト9は、こうした変更を太字で示しています。
リスト9. アップデートされたget_table_link() 関数とget_image_link() 関数
function get_table_link ( $title, $start, $step ) {
$link = "myHistory.addResource('table-$start-$step'); "
."x_get_table($start, $step, to_window); "
."return false;";
return '<a href="#" onclick="' . $link . '">' . $title
.'</a>';
}
function get_image_link ( $title, $index ) {
$link = "myHistory.addResource('image-$index'); "
."x_get_image($index, to_window); "
."return false;";
return '<a href="#" onclick="' . $link . '">' . $title .
'</a>';
} |
アプリケーションの中でSajaxコールが行われると、ページ上にHTMLを再生成するためのコールバックとしてto_window()関数が使われます。このテスト・アプリケーションでは、display() という関数を使いました(リスト8)。この関数は、ページ出力のアップデートと、履歴ボタンの状態のアップデートという、2つのタスクを処理します。今度は、既存のto_window()関数のボディーに次のファンクション・コールを追加します。
display_history_buttons(); |
この関数の定義をリスト10に示します。
リスト10. display_history_buttons() 関数
function display_history_buttons()
{
var str = '';
if (myHistory.hasPrev()) {
str += '<a href="#" onclick="do_back(); return false;">
<img src="icons/back_on.gif" alt="Back" /></a>';
} else {
str += '<img src="icons/back_off.gif" alt=" />';
}
if (myHistory.hasNext()) {
str += '<a href="#" onclick="do_forward(); return false;">
<img src="icons/forward_on.gif" alt="Forward" /></a>';
} else {
str += '<img src="icons/forward_off.gif" alt=" />';
}
str += '<a href="#" onclick="do_reload(); return false;">
<img src="icons/reload.gif" alt="Reload" /></a>';
document.getElementById("historybuttons").innerHTML = str;
} |
フォト・ギャラリー・アプリケーションの履歴の追跡を始める前には、ページに対するオンロード・イベント中にx_get_table()関数を呼ぶだけでした。これによって、表示すべき初期テーブルが、Sajaxを通して呼ばれたのです。
しかし今度は履歴スタックができたので、このアプリケーションがロードされる度に全く始めからやり直すのではなく、前回終わったところから再開するようにしたいと思います。そのために、ページがロードされると呼ばれるload_current()関数を作成して、アプリケーションを拡張します。「戻る」ボタンと「進む」ボタンのハンドラーを追加する際には、この関数も呼ばれ、履歴スタックに保存されているイベントIDからページがアップデートされるが分かります。
リスト11. load_current() 関数
function load_current()
{
// No existing history.
if (myHistory.stack.length == 0) {
x_get_table(to_window);
myHistory.addResource('table-0-5');
// Load from history.
} else {
var current = myHistory.getCurrent();
var params = current.split('-');
if (params[0] == 'table') {
x_get_table(params[1], params[2], to_window);
} else if (params[0] == 'image') {
x_get_image(params[1], to_window);
}
}
} |
それに従って、onloadハンドラーもアップデートされます。
window.onload = function () {
load_current();
};
|
最後に、履歴ボタン用のハンドラーを追加します(リスト12)。これらのハンドラーが、テスト・アプリケーションでのハンドラーに似ていることに注意してください。
リスト12. 履歴ボタンに対するイベント・ハンドラー
function do_back()
{
myHistory.go(-1);
load_current();
}
function do_forward()
{
myHistory.go(1);
load_current();
}
function do_reload()
{
myHistory.go(0);
} |
これで、フォト・ギャラリー・アプリケーションに履歴スタックを統合する作業は終わりです。できあがった作品を図3に示します。
図3. アクティブな履歴ボタンがフォト・ギャラリー・アプリケーションに統合されている
アプリケーションを開き、何度かクリックすると、履歴スタックとポインターがブラウザーのクッキーに保存されるのが分かります。
CHCurrent = 4
CHStack = table-0-5%2Cimage-1%2Cimage-2%2Cimage-3%2Ctable-3-5
|
もし皆さんがMozilla Firefoxを実行しており、Web Developer Toolbarエクステンションをダウンロードしてある場合には、これは特に容易です。
まとめ
ここでは、Ajaxアプリケーション内のイベントを追跡するために、カスタムの履歴スタックを作成する手順を説明しました。Webブラウザーで一般的な「戻る」「進む」「更新」ボタンをアプリケーションに追加すると、カスタムの履歴スタックをナビゲートできるようになります。
この課題を達成するに当たって、私達は問題を特定し、他のアプリケーションにも適用可能な再利用性の高いソリューションを作成しました。このソリューションでは、フォト・ギャラリーの中に履歴スタックを直接作るのではなく、クラスをテストするための単純なページを生成しました。それによって、1つのアプリケーション用に特化したソリューションではなく、他のAjaxアプリケーションで同じ問題を解決する場合にも再利用できるソリューションを作ることができました。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Part 2 source code | os-php-rad2.code.zip | 6.5KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
- Chris PederickによるWeb Developer Toolbarを使うと、クッキーやCSS、画像、フォーム、その他を見たり管理したりすることができます。これはMozillaのエクステンションのうち、最も便利なものの1つです。
- 皆さんの次期オープンソース開発プロジェクトを、IBM trial softwareを使って革新してください。ダウンロード、またはDVDで入手することができます。
議論するために
著者について  | 
|  | Mike Brittainは、ニューヨーク市にあるインタラクティブ・マーケティングの会社、ID Societyのdirector of technologyです。彼は10年以上の間、WebサイトやWebアプリケーションの開発にオープンソースの技術や言語を使ってきました。デンバー大学(University
of Denver)にて修士を取得しており、また同大学でスクリプト言語を教えた経験もあります。連絡先はmike@mikebrittain.comです。 |
記事の評価
|