作者:Brad Neuberg
譯者:boool
版權(quán)聲明:任何獲得Matrix授權(quán)的網(wǎng)站,轉(zhuǎn)載時(shí)請務(wù)必以超鏈接形式標(biāo)明文章原始出處和作者信息及本聲明
作者:Brad Neuberg;
boool
原文地址:
http://www.onjava.com/pub/a/onjava/2005/10/26/ajax-handling-bookmarks-and-back-button.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43972_AJAX.html
關(guān)鍵詞: ajax;bookmarks;back button
這篇文章描述了一個(gè)支持AJAX應(yīng)用書簽和回退按鈕的開源的javascript庫。在這個(gè)指南的最后,開發(fā)者將會(huì)得出一個(gè)甚至不是
Google Maps 或者
Gmail那樣處理的AJAX的解決方案:健壯的,可用的書簽和向前向后的動(dòng)作能夠象其他的web頁面一樣正確的工作。
AJAX:怎樣去控制書簽和回退按鈕 這篇文章說明了一個(gè)重要的成果,AJAX應(yīng)用目前面對著書簽和回退按鈕的應(yīng)用,描述了非常簡單的歷史庫(Really Simple History),一個(gè)開源的解決這類問題的框架,并提供了一些能夠運(yùn)行的例子。
這
篇文章描述的主要問題是雙重的,一是一個(gè)隱藏的html
表單被用作一個(gè)大而短生命周期的客戶端信息的session緩存,這個(gè)緩存對在這個(gè)頁面上前進(jìn)回退是強(qiáng)壯的。二是一個(gè)錨連接和隱藏的iframes的組合
用來截取和記錄瀏覽器的歷史事件,來實(shí)現(xiàn)前進(jìn)和回退的按鈕。這兩個(gè)技術(shù)都被用一個(gè)簡單的javascript庫來封裝,以利于開發(fā)者的使用。
存在的問題
書簽和回退按鈕在傳統(tǒng)的多頁面的web應(yīng)用上能順利的運(yùn)行。當(dāng)用戶在網(wǎng)站上沖浪時(shí),他們的瀏覽器地址欄能更新URL,這些URL可以被粘貼到的email或者添加到書簽以備以后的使用。回退和前進(jìn)按鈕也可以正常運(yùn)行,這可以使用戶在他們訪問的頁面間移動(dòng)。
AJAX應(yīng)用是與眾不同的,然而,他也是在單一web頁面上成熟的程序。瀏覽器不是為AJAX而做的—AJAX他捕獲過去的事件,當(dāng)web應(yīng)用在每個(gè)鼠標(biāo)點(diǎn)擊時(shí)刷新頁面。
在象Gmail那樣的AJAX軟件里,瀏覽器的地址欄正確的停留就象用戶在選擇和改變應(yīng)用的狀態(tài)時(shí),這使得作書簽到特定的應(yīng)用視圖里變得不可能。此外,如果用戶按下了他們的回退按鈕去返回上一個(gè)操作,他們會(huì)驚奇的發(fā)現(xiàn)瀏覽器將完全離開原來他所在的應(yīng)用的web頁面。
解決方案
開
源的Really Simply
History(RSH)框架解決了這些問題,他帶來了AJAX應(yīng)用的作書簽和控制前進(jìn)后退按鈕的功能。RSH目前還是beta版,在
Firefox1.0上,Netscape7及以上,和IE6及以上運(yùn)行。Safari現(xiàn)在還不支持(要得到更詳細(xì)的說明,請看我的weblog中的文章
Coding in Paradise: Safari: No DHTML History Possible).
目前存在的幾個(gè)AJAX框架可以幫助我們做書簽和發(fā)布?xì)v史,然而所有的框架都因?yàn)樗麄兊膶?shí)現(xiàn)而被幾個(gè)重要的bug困擾(請看
Coding in Paradise: AJAX History Libraries 得知詳情)。此外,許多AJAX歷史框架集成綁定到較大的庫上,比如Backbase 和 Dojo,這些框架提供了與傳統(tǒng)AJAX應(yīng)用不同的編程模型,強(qiáng)迫開發(fā)者去采用一整套全新的方式去獲得瀏覽器的歷史相關(guān)的功能。
相應(yīng)的,RSH是一個(gè)簡單的模型,能被包含在已經(jīng)存在的AJAX系統(tǒng)中。而且,Really Simple History庫使用了一些技巧去避免影響到其他歷史框架的bug.
Really Simple History框架由2個(gè)javascript類庫組成,分別叫DhtmlHistory 和 HistoryStorage.
DhtmlHistory
類提供了一個(gè)對AJAX應(yīng)用提取歷史的功能。.AJAX頁面add() 歷史事件到瀏覽器里,指定新的地址和關(guān)聯(lián)歷史數(shù)據(jù)。DhtmlHistory
類用一個(gè)錨的hash表更新瀏覽器現(xiàn)在的URL,比如#new-location
,然后用這個(gè)新的URL關(guān)聯(lián)歷史數(shù)據(jù)。AJAX應(yīng)用注冊他們自己到歷史監(jiān)聽器里,然后當(dāng)用戶用前進(jìn)和后退按鈕導(dǎo)航的時(shí)候,歷史事件被激發(fā),提供給瀏覽器新
的地址和調(diào)用add()持續(xù)保留數(shù)據(jù)。
第二個(gè)類HistoryStorage,允許開發(fā)者存儲(chǔ)任意大小的歷史數(shù)據(jù)。一般的頁面,當(dāng)一個(gè)用
戶導(dǎo)航到一個(gè)新的網(wǎng)站,瀏覽器會(huì)卸載和清除所有這個(gè)頁面的應(yīng)用和javascript狀態(tài)信息。如果用戶用回退按鈕返回過來了,所有的數(shù)據(jù)已經(jīng)丟失了。
HistoryStorage 類解決了這個(gè)問題,他有一個(gè)api
包含簡單的hashtable方法比如put(),get(),hasKey()。這些方法允許開發(fā)者在離開web頁面時(shí)存儲(chǔ)任意大小的數(shù)據(jù),當(dāng)用戶點(diǎn)了
回退按鈕返回時(shí),數(shù)據(jù)可以通過HistoryStorage 類被訪問。我們通過一個(gè)隱藏的表單域(a hidden form
field),利用瀏覽器即使在用戶離開web頁面也會(huì)自動(dòng)保存表單域值的這個(gè)特性,完成這個(gè)功能。
讓我們立即進(jìn)入一個(gè)簡單的例子吧。
示例1
首先,任何一個(gè)想使用Really Simple History框架的頁面必須包含(include)dhtmlHistory.js 腳本。
<!-- Load the Really Simple
History framework -->
<script type="text/javascript"
src="../../framework/dhtmlHistory.js">
</script>
DHTML
History 應(yīng)用也必須在和AJAX web頁面相同的目錄下包含一個(gè)叫blank.html 的指定文件,這個(gè)文件被Really Simple
History框架綁定而且對IE來說是必需的。另一方面,RSH使用一個(gè)hidden iframe
來追蹤和加入IE歷史的改變,為了正確的執(zhí)行功能,這個(gè)iframe需要指向一個(gè)真正的地址,不需要blank.html。
RSH框架創(chuàng)建了一個(gè)叫dhtmlHistory 的全局對象,作為操作瀏覽器歷史的入口。使用dhtmlHistory 的第一步需要在頁面加載后初始化這個(gè)對象。
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
然后,開發(fā)者使用dhtmlHistory.addListener()方法去訂閱歷史改變事件。這個(gè)方法獲取一個(gè)javascript回調(diào)方法,當(dāng)一個(gè)DHTML歷史改變事件發(fā)生時(shí)他將收到2個(gè)自變量,新的頁面地址,和任何可選的而且可以被關(guān)聯(lián)到這個(gè)事件的歷史數(shù)據(jù)。
indow.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
historyChange()方法是簡單易懂得,它是由一個(gè)用戶導(dǎo)航到一個(gè)新地址后收到的新地址(newLocation)和一個(gè)關(guān)聯(lián)到事件的可選的歷史數(shù)據(jù)historyData 構(gòu)成的。
/** Our callback to receive history change
events. */
function historyChange(newLocation,
historyData) {
debug("A history change has occurred: "
+ "newLocation="+newLocation
+ ", historyData="+historyData,
true);
}
上面用到的debug()方法是例子代碼中定義的一個(gè)工具函數(shù),在完整的下載例子里有。debug()方法簡單的在web頁面上打一條消息,第2個(gè)Boolean變量,在代碼里是true,控制一個(gè)新的debug消息打印前是否要清除以前存在的所有消息。
一個(gè)開發(fā)者使用add()方法加入歷史事件。加入一個(gè)歷史事件包括根據(jù)歷史的改變指定一個(gè)新的地址,就像"edit:SomePage"標(biāo)記, 還提供一個(gè)事件發(fā)生時(shí)可選的會(huì)被存儲(chǔ)到歷史數(shù)據(jù)historyData值.
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
// if this is the first time we have
// loaded the page...
if (dhtmlHistory.isFirstLoad()) {
debug("Adding values to browser "
+ "history", false);
// start adding history
dhtmlHistory.add("helloworld",
"Hello World Data");
dhtmlHistory.add("foobar", 33);
dhtmlHistory.add("boobah", true);
var complexObject = new Object();
complexObject.value1 =
"This is the first value";
complexObject.value2 =
"This is the second data";
complexObject.value3 = new Array();
complexObject.value3[0] = "array 1";
complexObject.value3[1] = "array 2";
dhtmlHistory.add("complexObject",
complexObject);
在add
()方法被調(diào)用后,新地址立刻被作為一個(gè)錨值顯示在用戶的瀏覽器的URL欄里。例如,一個(gè)AJAX
web頁面停留在http://codinginparadise.org/my_ajax_app,調(diào)用了dhtmlHistory.add
("helloworld", "Hello World Data" 后,用戶將在瀏覽器的URL欄里看到下面的地址
http://codinginparadise.org/my_ajax_app#helloworld
然
后他們可以把這個(gè)頁面做成書簽,如果他們使用這個(gè)書簽,你的AJAX應(yīng)用可以讀出#helloworld值然后使用她去初始化web頁面。Hash里的地
址值被Really Simple History 框架顯式的編碼和解碼(URL encoded and decoded)
(這是為了解決字符的編碼問題)
對當(dāng)AJAX地址改變時(shí)保存更多的復(fù)雜的狀態(tài)來說,historyData 比一個(gè)更容易的匹配一個(gè)
URL的東西更有用。他是一個(gè)可選的值,可以是任何javascript類型,比如Number, String, 或者 Object
類型。有一個(gè)例子是用這個(gè)在一個(gè)多文本編輯器(rich text
editor)保存所有的文本,例如,如果用戶從這個(gè)頁面漂移(或者說從這個(gè)頁面導(dǎo)航到其他頁面,離開了這個(gè)頁面)走。當(dāng)一個(gè)用戶再回到這個(gè)地址,瀏覽器
會(huì)把這個(gè)對象返回給歷史改變偵聽器(history change listener)。
開發(fā)者可以提供一個(gè)完全的
historyData 的javascript對象,用嵌套的對象objects和排列arrays來描繪復(fù)雜的狀態(tài)。只要是JSON
(JavaScript Object
Notation) 允許的那么在歷史數(shù)據(jù)里就是允許的,包括簡單數(shù)據(jù)類型和null型。DOM的對象和可編程的瀏覽器對象比如
XMLHttpRequest ,不會(huì)被保存。注意historyData
不會(huì)被書簽持久化,如果瀏覽器關(guān)掉,或者瀏覽器的緩存被清空,或者用戶清除歷史的時(shí)候,會(huì)消失掉。
使用dhtmlHistory
最后一步,是isFirstLoad()
方法。如果你導(dǎo)航到一個(gè)web頁面,再跳到一個(gè)不同的頁面,然后按下回退按鈕返回起始的網(wǎng)站,第一頁將完全重新裝載,并激發(fā)onload事件。這樣能產(chǎn)生
破壞性,當(dāng)代碼在第一次裝載時(shí)想要用某種方式初始化頁面的時(shí)候,不會(huì)再刷新頁面。isFirstLoad()
方法讓區(qū)別是最開始第一次裝載頁面,還是相對的,在用戶導(dǎo)航回到他自己的瀏覽器歷史中記錄的網(wǎng)頁時(shí)激發(fā)load事件,成為可能。
在例子代碼中,我們只想在第一次頁面裝載的時(shí)候加入歷史事件,如果用戶在第一次裝載后,按回退按鈕返回頁面,我們就不想重新加入任何歷史事件。
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
// if this is the first time we have
// loaded the page...
if (dhtmlHistory.isFirstLoad()) {
debug("Adding values to browser "
+ "history", false);
// start adding history
dhtmlHistory.add("helloworld",
"Hello World Data");
dhtmlHistory.add("foobar", 33);
dhtmlHistory.add("boobah", true);
var complexObject = new Object();
complexObject.value1 =
"This is the first value";
complexObject.value2 =
"This is the second data";
complexObject.value3 = new Array();
complexObject.value3[0] = "array 1";
complexObject.value3[1] = "array 2";
dhtmlHistory.add("complexObject",
complexObject);
讓
我們繼續(xù)使用historyStorage 類。類似dhtmlHistory
,historyStorage通過一個(gè)叫historyStorage的單一全局對象來顯示他的功能,這個(gè)對象有幾個(gè)方法來偽裝成一個(gè)hash
table, 象put(keyName, keyValue), get(keyName), and
hasKey(keyName).鍵名必須是字符,同時(shí)鍵值可以是復(fù)雜的javascript對象或者甚至是xml格式的字符。在我們源碼source
code的例子中,我們put() 簡單的XML 到historyStorage 在頁面第一次裝載時(shí)。
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
// if this is the first time we have
// loaded the page...
if (dhtmlHistory.isFirstLoad()) {
debug("Adding values to browser "
+ "history", false);
// start adding history
dhtmlHistory.add("helloworld",
"Hello World Data");
dhtmlHistory.add("foobar", 33);
dhtmlHistory.add("boobah", true);
var complexObject = new Object();
complexObject.value1 =
"This is the first value";
complexObject.value2 =
"This is the second data";
complexObject.value3 = new Array();
complexObject.value3[0] = "array 1";
complexObject.value3[1] = "array 2";
dhtmlHistory.add("complexObject",
complexObject);
// cache some values in the history
// storage
debug("Storing key 'fakeXML' into "
+ "history storage", false);
var fakeXML =
'<?xml version="1.0" '
+ 'encoding="ISO-8859-1"?>'
+ '<foobar>'
+ '<foo-entry/>'
+ '</foobar>';
historyStorage.put("fakeXML", fakeXML);
}
然后,如果用戶從這個(gè)頁面漂移走(導(dǎo)航走)又通過返回按鈕返回了,我們可以用get()提出我們存儲(chǔ)的值或者用haskey()檢查他是否存在。
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
// if this is the first time we have
// loaded the page...
if (dhtmlHistory.isFirstLoad()) {
debug("Adding values to browser "
+ "history", false);
// start adding history
dhtmlHistory.add("helloworld",
"Hello World Data");
dhtmlHistory.add("foobar", 33);
dhtmlHistory.add("boobah", true);
var complexObject = new Object();
complexObject.value1 =
"This is the first value";
complexObject.value2 =
"This is the second data";
complexObject.value3 = new Array();
complexObject.value3[0] = "array 1";
complexObject.value3[1] = "array 2";
dhtmlHistory.add("complexObject",
complexObject);
// cache some values in the history
// storage
debug("Storing key 'fakeXML' into "
+ "history storage", false);
var fakeXML =
'<?xml version="1.0" '
+ 'encoding="ISO-8859-1"?>'
+ '<foobar>'
+ '<foo-entry/>'
+ '</foobar>';
historyStorage.put("fakeXML", fakeXML);
}
// retrieve our values from the history
// storage
var savedXML =
historyStorage.get("fakeXML");
savedXML = prettyPrintXml(savedXML);
var hasKey =
historyStorage.hasKey("fakeXML");
var message =
"historyStorage.hasKey('fakeXML')="
+ hasKey + "<br>"
+ "historyStorage.get('fakeXML')=<br>"
+ savedXML;
debug(message, false);
}
prettyPrintXml() 是一個(gè)第一在例子源碼full example source code中的工具方法。這個(gè)方法準(zhǔn)備簡單的xml顯示在web page ,方便調(diào)試。
注
意數(shù)據(jù)只是在使用頁面的歷史時(shí)被持久化,如果瀏覽器關(guān)閉了,或者用戶打開一個(gè)新的窗口又再次鍵入了ajax應(yīng)用的地址,歷史數(shù)據(jù)對這些新的web頁面是不
可用的。歷史數(shù)據(jù)只有在用前進(jìn)或回退按鈕時(shí)才被持久化,而且在用戶關(guān)閉瀏覽器或清空緩存的時(shí)候會(huì)消失掉。想真正的長時(shí)間的持久化,請看Ajax
MAssive Storage System (AMASS).
我們的簡單示例已經(jīng)完成。演示他(Demo it)或者下載全部的源代碼(download the full source code.)
示例2
我
們的第2個(gè)例子是一個(gè)簡單的模擬ajax email 應(yīng)用的示例,叫O'Reilly Mail,類似Gmail. O'Reilly
Mail描述了怎樣使用dhtmlHistory類去控制瀏覽器的歷史,和怎樣使用historyStorage對象去緩存歷史數(shù)據(jù)。
O'Reilly
Mail 用戶接口(user interface)有兩部分。在頁面的左邊是一個(gè)有不同email文件夾和選項(xiàng)的菜單,例如
收件箱,草稿,等等。當(dāng)一個(gè)用戶選擇了一個(gè)菜單項(xiàng),比如收件箱,我們用這個(gè)菜單項(xiàng)的內(nèi)容更新右邊的頁面。在一個(gè)實(shí)際應(yīng)用中,我們會(huì)遠(yuǎn)程取得和顯示選擇的信
箱內(nèi)容,不過在O'Reilly Mail里,我們簡單的顯示選擇的選項(xiàng)。
O'Reilly Mail使用Really Simple History 框架向?yàn)g覽器歷史里加入菜單變化和更新地址欄,允許用戶利用瀏覽器的回退和前進(jìn)按鈕對應(yīng)用做書簽和跳到上一個(gè)變化的菜單。
我
們加入一個(gè)特別的菜單項(xiàng),地址簿,來描繪historyStorage
能夠怎樣被使用。地址簿是一個(gè)由聯(lián)系的名字電子郵件和地址組成的javascript數(shù)組,在一個(gè)真實(shí)的應(yīng)用里我們會(huì)取得他從一個(gè)遠(yuǎn)程的服務(wù)器。不過,在
O'Reilly Mail里,我們在本地創(chuàng)建這個(gè)數(shù)組,加入幾個(gè)名字電子郵件和地址,然后把他們存儲(chǔ)在historyStorage
對象里。如果用戶離開了這個(gè)web頁面以后又返回的話,O'Reilly Mail應(yīng)用重新從緩存里得到地址簿,勝過(不得不)再次訪問遠(yuǎn)程服務(wù)器。
地址簿是在我們的初始化initialize()方法里存儲(chǔ)和重新取得的
/** Our function that initializes when the page
is finished loading. */
function initialize() {
// initialize the DHTML History framework
dhtmlHistory.initialize();
// add ourselves as a DHTML History listener
dhtmlHistory.addListener(handleHistoryChange);
// if we haven't retrieved the address book
// yet, grab it and then cache it into our
// history storage
if (window.addressBook == undefined) {
// Store the address book as a global
// object.
// In a real application we would remotely
// fetch this from a server in the
// background.
window.addressBook =
["Brad Neuberg 'bkn3@columbia.edu'",
"John Doe 'johndoe@example.com'",
"Deanna Neuberg 'mom@mom.com'"];
// cache the address book so it exists
// even if the user leaves the page and
// then returns with the back button
historyStorage.put("addressBook",
addressBook);
}
else {
// fetch the cached address book from
// the history storage
window.addressBook =
historyStorage.get("addressBook");
}
處
理歷史變化的代碼是簡單的。在下面的代碼中,當(dāng)用戶不論按下回退還是前進(jìn)按鈕handleHistoryChange
都被調(diào)用。我們得到新的地址(newLocation)
使用他更新我們的用戶接口來改變狀態(tài),通過使用一個(gè)叫displayLocation的O'Reilly Mail的工具方法。
/** Handles history change events. */
function handleHistoryChange(newLocation,
historyData) {
// if there is no location then display
// the default, which is the inbox
if (newLocation == "") {
newLocation = "section:inbox";
}
// extract the section to display from
// the location change; newLocation will
// begin with the word "section:"
newLocation =
newLocation.replace(/section\:/, "");
// update the browser to respond to this
// DHTML history change
displayLocation(newLocation, historyData);
}
/** Displays the given location in the
right-hand side content area. */
function displayLocation(newLocation,
sectionData) {
// get the menu element that was selected
var selectedElement =
document.getElementById(newLocation);
// clear out the old selected menu item
var menu = document.getElementById("menu");
for (var i = 0; i < menu.childNodes.length;
i++) {
var currentElement = menu.childNodes[i];
// see if this is a DOM Element node
if (currentElement.nodeType == 1) {
// clear any class name
currentElement.className = "";
}
}
// cause the new selected menu item to
// appear differently in the UI
selectedElement.className = "selected";
// display the new section in the right-hand
// side of the screen; determine what
// our sectionData is
// display the address book differently by
// using our local address data we cached
// earlier
if (newLocation == "addressbook") {
// format and display the address book
sectionData = "<p>Your addressbook:</p>";
sectionData += "<ul>";
// fetch the address book from the cache
// if we don't have it yet
if (window.addressBook == undefined) {
window.addressBook =
historyStorage.get("addressBook");
}
// format the address book for display
for (var i = 0;
i < window.addressBook.length;
i++) {
sectionData += "<li>"
+ window.addressBook[i]
+ "</li>";
}
sectionData += "</ul>";
}
// If there is no sectionData, then
// remotely retrieve it; in this example
// we use fake data for everything but the
// address book
if (sectionData == null) {
// in a real application we would remotely
// fetch this section's content
sectionData = "<p>This is section: "
+ selectedElement.innerHTML + "</p>";
}
// update the content's title and main text
var contentTitle =
document.getElementById("content-title");
var contentValue =
document.getElementById("content-value");
contentTitle.innerHTML =
selectedElement.innerHTML;
contentValue.innerHTML = sectionData;
}
演示(Demo)O'Reilly Mail或者下載(download)O'Reilly Mail的源代碼。
結(jié)束語
你現(xiàn)在已經(jīng)學(xué)習(xí)了使用Really Simple History API 讓你的AJAX應(yīng)用響應(yīng)書簽和前進(jìn)回退按鈕,而且有代碼可以作為創(chuàng)建你自己的應(yīng)用的素材。我熱切地期待你利用書簽和歷史的支持完成你的AJAX創(chuàng)造。
資源
·onjava.com:
onjava.com
·Matrix-Java開發(fā)者社區(qū):
http://www.matrix.org.cn/
·
Download all sample code for this article.
·
Download the Really Simple History framework.
·Demo O'Reilly Mail or
download the O'Reilly Mail source code. The full example download also includes more examples for you to play with.
·
Coding in Paradise:
The author's weblog, covering AJAX, DHTML, and Java techniques and new
developments in collaborative technologies, such as WikiWikis.
感謝
特別的要感謝每個(gè)檢閱這篇文章的the Really Simple History框架的人:
Michael
Eakes, Jeremy Sevareid, David Barrett, Brendon Wilson, Dylan Parker,
Erik Arvidsson, Alex Russell, Adam Fisk, Alex Lynch, Joseph Hoang Do,
Richard MacManus, Garret Wilson, Ray Baxter, Chris Messina, and David
Weekly.