摘要
Ajax應(yīng)用程序由于其豐富的功能、交互性以及快速的響應(yīng)能力而得到人們的普遍贊許。它可以使用XMLHttpRequest對象動態(tài)地加載數(shù)據(jù),而不是加載新的頁面。在它大肆進行宣傳以及許多人興奮的同時,有評論指出,Ajax應(yīng)用程序丟失了瀏覽器的一些重要功能,包括對后退前進按鈕的支持。
本文將首先闡明為什么在Ajax應(yīng)用程序中除非顯式地構(gòu)建后退/前進按鈕以及其它瀏覽器功能,否則它們將無法運行的原因。然后,我們將簡要介紹開發(fā)人員如何解決這些問題。最后,我們將看到有關(guān)Backbase Ajax引擎如何支持后退/前進按鈕以及其它標(biāo)準(zhǔn)瀏覽器功能的詳細情況。
Ajax應(yīng)用程序是否需要后退按鈕?
Ajax承諾,可以讓開發(fā)人員完全基于標(biāo)準(zhǔn)的Web瀏覽器技術(shù)(通常是指DHTML)創(chuàng)建在視覺上吸引人的、高度交互式的Web應(yīng)用程序。
以前開發(fā)人員不得不在功能豐富(具有高度交互性的、吸引人的用戶界面)和易于到達(不需要進行客戶端安裝就可以工作在所有Web瀏覽器下的前端)二者之中作出選擇。而Ajax應(yīng)用程序應(yīng)該能夠產(chǎn)生既“功能豐富”又“易于到達”的前端。
但是一個界面怎樣才算是“功能豐富”的,而一個應(yīng)用程序又怎樣才是“易于到達”的呢?
很難精確地定義“功能豐富”的含義,但是卻很容易直覺地認(rèn)識到:當(dāng)您看到一個界面時,您就會知道它是不是功能豐富的。象Microsoft Office之類的桌面應(yīng)用程序就是功能豐富的。功能豐富的界面使用諸如選項卡和上下文菜單這樣的高級UI控件。這樣的界面提供一些高級交互方法。例如,拖放、對關(guān)注的UI元素進行高亮顯示等。傳統(tǒng)的瀏覽器應(yīng)用程序是功能不豐富的。它們僅限于諸如表單之類的簡單控件,交互主要是由到新頁面的單擊鏈接組成。我們只要看看微軟的電子郵件客戶端就可以看出功能豐富和功能不豐富的區(qū)別:Outlook是功能豐富的,而Hotmail就是功能不豐富的。
Ajax應(yīng)用程序已經(jīng)由于功能豐富而得到人們的普遍贊許。Google的Gmail就是其中最具代表性的例子。Google所開發(fā)的其它Ajax應(yīng)用程序(Google Suggest、 Google Map)、微軟即將推出的名為“Kahuna”的Web郵件客戶端以及Backbase RSS Reader都包含了一些高級控件和交互模塊。可查看Dan Grossman的Top 10 Ajax Applications列表,其中給出了一個讓人印象深刻的功能豐富界面的列表。
通過前面的討論,可以說Ajax應(yīng)用程序很明顯滿足“功能豐富”的標(biāo)準(zhǔn)。那么它是不是“易于到達”的呢?
首先,最基本的是,只有界面在Web瀏覽器中運行的應(yīng)用程序才是“易于到達”的。Ajax應(yīng)用程序是基于瀏覽器標(biāo)準(zhǔn)的,因此可以通過Web瀏覽器來訪問。
但是,僅僅可以通過Web瀏覽器訪問還不夠。Jakob Nielson在他2000年的文章Flash: 99% Bad中指出,F(xiàn)lash“打破了Web的基本交互方式”。終端用戶希望在使用Web應(yīng)用程序時所面對的是特定的交互方式。應(yīng)用程序需要遵從傳統(tǒng)的Web交互方式,并提供以下的可用功能:
- 后退和前進按鈕可以正常工作,以便終端用戶可以導(dǎo)航到歷史記錄頁面。
- 用戶應(yīng)該可以創(chuàng)建書簽。
- 支持深鏈接,以保證終端用戶可以將這個頁面通過電子郵件發(fā)送給朋友和同事。
- 刷新按鈕可以正常工作,以便刷新當(dāng)前的狀態(tài)而不是重新初始化應(yīng)用程序。
- 開發(fā)人員可以使用“查看源文件”看到源代碼。
- 終端用戶可以使用“查找”對頁面進行搜索。
- 搜索引擎可以為頁面做索引并創(chuàng)建到搜索項的深鏈接。
再看一下Top 10 Ajax Applications列表,我們可以看出,之前討論的大多數(shù)Ajax應(yīng)用程序的確打破了標(biāo)準(zhǔn)的Web交互方式。在下一節(jié)中,我們將討論為什么許多Ajax應(yīng)用程序會這么做。
為什么Ajax應(yīng)用程序常常會使后退按鈕無法正常工作?
我們所說的Web基于以下三個原則:
- 使用 (D)HTML來定義界面
- 使用HTTP實現(xiàn)客戶端與服務(wù)器間的通訊
- 使用URI進行尋址
Ajax編程突破了由以上原則所帶來的種種限制,使得界面功能更加豐富。正如我在以前的文章A Backbase Ajax Front-end for J2EE Applications(中文版)中所介紹的那樣,Ajax廣泛使用了JavaScript(“J”)以創(chuàng)建功能豐富的UI組件和交互性。Ajax還引入了異步的XML通信(“A”和“X”),也就是使用XMLHttpRequest對象導(dǎo)入新的數(shù)據(jù)和表示邏輯而不必刷新頁面。然而,目前的Ajax模型并沒有解決如何處理URI的問題。
Ajax應(yīng)用程序?qū)?D)HTML和HTTP的使用方式做了改變,而這種改變帶來的直接結(jié)果就是后退按鈕和Web的基本交互方式的其它元素?zé)o法正常工作了。在本節(jié)的其余部分,我將說明如何通過以Ajax的方式處理URI來解決上述問題。首先我們來看看在傳統(tǒng)的Web應(yīng)用程序中URI是如何與用戶交互相關(guān)聯(lián)的。
從技術(shù)方面來說,用戶交互是指用戶界面狀態(tài)的一次更改。狀態(tài)改變由終端用戶發(fā)起。瀏覽器客戶端通過向服務(wù)器發(fā)出頁面請求來處理狀態(tài)更改(REST法則)。服務(wù)器將發(fā)送新的頁面和新的URI到客戶端以生成新的界面狀態(tài)。簡單地說,每個用戶交互都是通過會導(dǎo)致如下結(jié)果的服務(wù)器往返來處理的:
這些Web功能之所以能夠被使用,是因為瀏覽器在它的歷史記錄堆棧中記錄了連續(xù)的URI,并在地址欄中向終端用戶顯示當(dāng)前URI,用戶可以通過地址欄復(fù)制URI,并將其發(fā)送給朋友。當(dāng)用戶單擊后退按鈕或者向瀏覽器的地址欄中粘貼一個來自于電子郵件的URI時,就會觸發(fā)一次到服務(wù)器的往返。因為服務(wù)器負責(zé)狀態(tài)管理,所以它就可以生成相應(yīng)的頁面。
Ajax應(yīng)用程序與傳統(tǒng)的Web應(yīng)用程序之間的主要區(qū)別在,Ajax應(yīng)用程序可以處理用戶的交互而無需頁面重新加載。例如,通過XMLHttpRequest對象從服務(wù)器載入數(shù)據(jù),或者使用JavaScript來處理拖放客戶端。
在上面的兩個例子中,狀態(tài)改變了,但是卻沒有生成新的URI。因此,單擊后退按鈕或刷新按鈕會產(chǎn)生意外的結(jié)果,在地址欄中也不會有深鏈接的URI。
為了提供傳統(tǒng)的Web可用功能,Ajax應(yīng)用程序需要以類似于服務(wù)器處理傳統(tǒng)的Web應(yīng)用程序的方式來處理URI客戶端。Ajax應(yīng)用程序需要實現(xiàn)以下功能:
- 當(dāng)客戶端狀態(tài)發(fā)生改變時,生成一個URI,并將其發(fā)送到瀏覽器
- 當(dāng)瀏覽器請求新的URI時可以重新創(chuàng)建狀態(tài)。
實現(xiàn)以上功能后,瀏覽器的歷史記錄就可以正常工作,瀏覽器的地址欄就可以顯示URI,當(dāng)然,您也就可以將它發(fā)送給朋友了。這里還有另外一個難點,那就是如何確定Ajax引擎什么時候需要實現(xiàn)以上功能(例如,哪一次狀態(tài)改變需要創(chuàng)建新的URI)。在傳統(tǒng)的模式中,每一次頁面刷新都對應(yīng)著一次URI更新。而在Ajax模式中,每一個客戶端事件都將新的URI添加到瀏覽器堆棧中。交互設(shè)計者和開發(fā)人員將不得不做出決定:哪一次狀態(tài)改變是有意義的。只對有意義的狀態(tài)改變才需要生成URI。
下面我們對提供Web可用功能的Ajax應(yīng)用程序在客戶端需要實現(xiàn)的功能做一下總結(jié):
- 創(chuàng)建歷史記錄
- 保存有意義的狀態(tài)
- 生成相應(yīng)的URI
- 將這個URI添加到瀏覽器的堆棧中
- 恢復(fù)歷史記錄
- 檢測URI的改變
- 通過URI重新創(chuàng)建狀態(tài)
在Ajax中支持后退按鈕的基本設(shè)計思想
在這一節(jié)中,我們將討論在Ajax應(yīng)用程序中支持后退按鈕所需的基本步驟,并給出說明所需步驟的簡單示例代碼。有關(guān)如何以跨瀏覽器兼容的方式實現(xiàn)后退按鈕支持的更完整討論可以參見Mike Stenhouse在Content with Style以及Brad Neuberg在OnJava發(fā)表的文章。這兩篇文章也是我比較喜歡的。
簡單示例程序如圖1所示,在界面中將有一個選擇框,它有兩個值:“Year 1”和“Year 2”。對于這個程序,我們將在選擇框值發(fā)生改變時跟蹤歷史記錄。這意味著用戶可以首先選擇“Year 2”然后單擊后退按鈕后退到先前的選擇。

圖1.帶有選擇框的簡單示例程序
示例程序最初是一個帶有JavaScript getter和setter(用于選擇框值)的簡單HTML表單:
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
}
</script>
</head>
<body>
<form name=make_history>
<select name=change_year>
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
我們將首先實現(xiàn)第一個要求:創(chuàng)建狀態(tài)的歷史記錄。正如我們前面所提到的,這個要求包含以下三個步驟:
- 創(chuàng)建歷史記錄
- 保存有意義的狀態(tài)
- 生成相應(yīng)的URI
- 將這個URI添加到瀏覽器的堆棧中
我們希望能夠保存選擇框的每一次更改。因此我們將創(chuàng)建新的包含選擇框狀態(tài)信息的URI。
為了遵循Internet標(biāo)準(zhǔn),我們將使用URI的碎片標(biāo)識符部分。按照IETF RFC 3986的規(guī)定,“……作為客戶端間接引用的主要形式,碎片標(biāo)識符在信息檢索系統(tǒng)中起著特殊的作用,〈……〉碎片標(biāo)識符在解除引用之前與URI的其余部分是分離的,因此,碎片本身中的標(biāo)識信息只被用戶代理所廢棄,而不考慮URI方案……”。
使用碎片標(biāo)識符,我們可以創(chuàng)建一個“Ajax-URI”,其中的客戶端部分和服務(wù)器端部分使用“#”隔開。
JavaScript提供了window.location()函數(shù),以便通過URI更新瀏覽器的歷史記錄和地址。此外,我們可以使用window.location.hash()直接訪問碎片標(biāo)識符。
在下面的代碼片斷中,您可以看到如何通過對選擇框使用onchange事件處理程序來擴展我們的代碼,該處理程序使用一個“Ajax-URI”來更新瀏覽器歷史記錄及地址欄。
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
function makeHistory(newHash)
{
window.location.hash = newHash;
}
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
}
</script>
</head>
<body>
<form name=make_history>
<select name=change_year
onchange=
"return makeHistory(reportOptionValue())">
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
正如我們在圖2中所看到的,選擇框的每一次變動都將導(dǎo)致瀏覽器地址的更新。請注意,在需要使用隱藏幀以獲取正確的行為的Internet Explorer (IE)中會存在一些問題,詳細情況還是請參見Mike Stenhouse和Brad Neuberg的文章。

圖2.狀態(tài)變化時歷史記錄堆棧被更新
我們現(xiàn)在有了一個在選擇框的值發(fā)生變化時創(chuàng)建新URI的事件處理程序。新URI使用碎片標(biāo)識符存儲重新創(chuàng)建先前狀態(tài)所需的信息。現(xiàn)在我們可以著手實現(xiàn)下一個功能了。
- 恢復(fù)歷史記錄
- 檢測URI的更改
- 通過URI重新創(chuàng)建狀態(tài)
在第一步中,我們通過window.location.hash()函數(shù)更新了客戶端的URI。這個調(diào)用并不會產(chǎn)生服務(wù)器的往返,也不會導(dǎo)致頁面刷新。因此,我們需要使用Ajax的方法(在客戶端)處理URI的改變。
首先需要增加一個輪詢函數(shù),以定時檢查瀏覽器歷史記錄中的URI。我將在頁面的onload事件中使用pollHash()函數(shù),每隔1000毫秒它將重新執(zhí)行一次。
這個輪詢函數(shù)將調(diào)用handleHistory()函數(shù),后者檢查在上一次檢查之后URI是否改變了。我們將借助一個名為expectedHash的全局變量來實現(xiàn)。
最后一部分是確定URI是否發(fā)生了改變,這種改變由選擇框中的事件處理程序引起,或者是因為終端用戶單擊了后退按鈕而造成。我們通過在選擇框的事件處理程序中設(shè)置expectedHash來達到此目的。
<html>
<head>
<script language="JavaScript" type="text/JavaScript">
var expectedHash = "";
function makeHistory(newHash)
{
window.location.hash = newHash;
expectedHash = window.location.hash;
return true;
}
function reportOptionValue()
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
return mySelect.options[mySelect.selectedIndex].value;
}
function setOptionValue(value)
{
var myForm = document.make_history;
var mySelect = myForm.change_year;
mySelect.options[value-1].selected = true;
return true;
}
function handleHistory()
{
if ( window.location.hash != expectedHash )
{
expectedHash = window.location.hash;
var newoption = expectedHash.substring(6);
setOptionValue( newoption );
}
return true;
}
function pollHash() {
handleHistory();
window.setInterval("handleHistory()", 1000);
return true;
}
</script>
</head>
<body language="JavaScript"
onload="return pollHash()">
<form name=make_history>
<select name=change_year
onchange="return makeHistory(reportOptionValue())">
<option value="year_1">Year 1</option>
<option value="year_2">Year 2</option>
</select>
</form>
</body>
</html>
到此,我們的示例程序就完成了。在這個程序中,我們演示了如何在URI中記錄狀態(tài),如何將URI添加到瀏覽器的歷史記錄堆棧中,如何從后退按鈕檢測地址變動,以及最終如何重新創(chuàng)建所需的狀態(tài)。
這個示例程序還缺少以下功能:
- 對使用隱藏幀的IE的支持
- 更多的固定URI(這個示例程序只用于選擇框選項少于10的情況)
- 在構(gòu)造時注冊初始狀態(tài)
以一種兼容所有瀏覽器的健壯方式實現(xiàn)對所有傳統(tǒng)的Web可用功能的處理不是一件容易的事。一種替代方法是使用對這些功能提供了內(nèi)置支持的Ajax工具包。
在下一節(jié)中,我們將描述Backbase Ajax引擎如何提供這些功能。我參考了Ajax forum on the Backbase DevNet上的實現(xiàn)。
案例分析:包含后退按鈕和深鏈接的Ajax論壇
Backbase Ajax引擎是一個成熟的、功能豐富的Ajax軟件包。對所有傳統(tǒng)Web可用功能的支持是Backbase的優(yōu)點之一。Backbase DevNet包含了為開發(fā)人員提供的、與Backbase和Ajax有關(guān)的信息。而開發(fā)人員論壇是DevNet的一部分。Backbase Web應(yīng)用程序(包括DevNet及其討論論壇)是使用Backbase構(gòu)建的。為了演示該論壇功能豐富和易于到達的特點,我們將逐步遍歷論壇的典型用例:
- 開發(fā)人員瀏覽論壇,閱讀不同的主題。
- 開發(fā)人員復(fù)制這個主題的URI,將其粘貼到電子郵件中并發(fā)送給朋友。這個朋友從電子郵件中復(fù)制這個URI到一個瀏覽器中并打開同一論壇主題。
- 開發(fā)人員單擊后退按鈕以閱讀以前的主題。
進行幾次用戶交互后的論壇界面狀態(tài)
我們來看看開發(fā)人員來到“BXML”論壇并選中名為“Issue with vertical and horizontal menus”的貼子之后,論壇界面的狀態(tài)以及地址欄中的對應(yīng)URI是什么樣的情況。
論壇和貼子被選中,并被高亮顯示。討論的主題被顯示出來以供閱讀。在URI的碎片標(biāo)識符中包含了所有的相關(guān)信息。在#后面,我們看到了為書簽和深鏈接而記錄的完整狀態(tài):“forum”表示開發(fā)人員在瀏覽這個Web站點的論壇部分;“forum=2”表示當(dāng)前選中的是BXML論壇,“thread=211”記錄了當(dāng)前所選擇的主題。最后,方括號中的“[5]”表示與書簽結(jié)合的對多個后退和前進步驟的處理。

圖3.具有Ajax URI的論壇初始狀態(tài) (單擊圖片查看大圖)
訪問Backbase論壇,您就可以看到URI如何隨著每次狀態(tài)改變而更新,即使更新是在客戶端進行處理的,或者牽涉到通過XMLHttpRequest對象進行部分頁面更新。
在新的瀏覽器窗口內(nèi)重新創(chuàng)建論壇界面的狀態(tài)
現(xiàn)在讓我們看看當(dāng)開發(fā)人員將當(dāng)前URI發(fā)送給朋友時會發(fā)生什么情況。這個朋友在瀏覽器窗口中打開了這個URI,期望能看到相同的界面狀態(tài)。需要在新的瀏覽器中重新創(chuàng)建該狀態(tài)。對于本文,我是從一個Firefox窗口中復(fù)制URI到一個新打開的IE窗口中。
在地址欄中輸入URI首先會產(chǎn)生一個服務(wù)器端的請求。使用“#”前的部分,會加載Backbase.com,在這一過程中,Backbase Ajax引擎也就實現(xiàn)了初始化。活動的Backbase引擎會閱讀URI中“#”后的部分。通過這些信息,Backbase引擎會轉(zhuǎn)到“論壇(forum)”部分,并選定BXML論壇(id=2)中的第211個主題,從而創(chuàng)建相應(yīng)的狀態(tài)。不需要頁面的刷新,只需從服務(wù)器中加載附加的內(nèi)容并在客戶端部分地更新界面,就可以實現(xiàn)了。
在后續(xù)的瀏覽器功能的處理中,新的URI被添加到瀏覽器歷史記錄中,這個新的URI既可以在地址欄中使用,也可以用來做深鏈接。“[0]”表示沒有可返回(使用后退按鈕)的先前狀態(tài)。

圖4.在新的瀏覽器窗口中重新創(chuàng)建論壇狀態(tài)(單擊圖片查看大圖)
用戶單擊后退按鈕后的論壇界面狀態(tài)
第一步我們研究了URI如何隨著由用戶交互所觸發(fā)的界面狀態(tài)更改而更新。下面我們將看到相反的情況:用戶請求新的URI,相應(yīng)的狀態(tài)被重新創(chuàng)建。
通過單擊后退按鈕,用戶要求返回先前閱讀的頁面。瀏覽器通過從歷史記錄堆棧中找回先前的URI來響應(yīng)后退按鈕。Backbase Ajax引擎將監(jiān)測這一變化,從歷史記錄中讀取新的URI,并來到“論壇”部分選定BXML論壇(id=2)中的第192個主題,從而重新構(gòu)建相應(yīng)的狀態(tài)。新的URI將按照上述語義顯示在地址欄中。
到這里,我們的案例分析也就結(jié)束了。

圖5.單擊后退按鈕后的論壇狀態(tài)(單擊圖片查看大圖)
Ajax程序確實需要后退按鈕!
在過去的幾年中,Web開發(fā)人員因為市場要求“易于到達”并愿意接受“功能豐富”方面的犧牲,所以選擇構(gòu)建Web界面。然而,當(dāng)前Ajax受到的普遍關(guān)注清楚地顯示出這種情況實際上只是暫時的。市場現(xiàn)在強烈要求Web程序也能像桌面應(yīng)用程序那樣具有豐富的功能、交互性以及敏捷的響應(yīng)能力。
但是,終端用戶已經(jīng)習(xí)慣了Web交互方式。使用常見模式與任何Web界面進行交互可以提高生產(chǎn)力。終端用戶期望后退/前進按鈕和刷新按鈕能正常工作,可以創(chuàng)建書簽和深鏈接,可以查看源文件,使用“查找”對頁面進行搜索,而且搜索引擎可以對Ajax應(yīng)用程序建立索引。
Ajax社區(qū)必須知道:正如本文所述,在Ajax應(yīng)用程序中提供對后退/前進按鈕以及其它傳統(tǒng)瀏覽器功能的支持的技術(shù)是存在的。雖然實現(xiàn)起來并不容易,而且會增加成本,但是Ajax社區(qū)的成功需要將傳統(tǒng)的瀏覽器功能構(gòu)建到Ajax應(yīng)用程序中。因此,我強烈呼吁Ajax開發(fā)人員構(gòu)建支持這些功能的Ajax應(yīng)用程序!
結(jié)束語
在本文中,我著重闡明了Ajax應(yīng)用程序為什么需要遵從傳統(tǒng)的Web交互方式并提供傳統(tǒng)的Web可用功能。我確定可以通過創(chuàng)建在碎片標(biāo)識符中包含客戶端狀態(tài)信息的“Ajax URI” ,從而將這些功能編程到Ajax應(yīng)用程序中。
閱讀相關(guān)代碼,您會發(fā)現(xiàn),由于狀態(tài)處理代碼通常非常重要,再加上不同瀏覽器之間常常不兼容,實現(xiàn)完整的通用解決方案是相當(dāng)困難的。而Backbase Ajax引擎通過開箱即用地提供所需功能,為該問題提供了一種解決方案。