這篇文章描述了一個支援Ajax應用書籤和回退按鈕的開源的javascript庫。在這個指南的最後,開發人員將會得出一個甚至不是Google Maps 或者 Gmail那樣處理的Ajax的解決方案:健壯的,可用的書籤和向前向後的動作能夠象其他的web頁面一樣正確的工作。
Ajax:怎樣去控制書籤和回退按鈕 這篇文章說明了一個重要的成果,Ajax應用目前面對著書籤和回退按鈕的應用,描述了非常簡單的曆史庫(Really Simple History),一個開源的解決這類問題的架構,並提供了一些能夠啟動並執行例子。
這篇文章描述的主要問題是雙重的,一是一個隱藏的html 表單被用作一個大而短生命週期的用戶端資訊的session緩衝,這個緩衝對在這個頁面上前進回退是強壯的。二是一個錨串連和隱藏的iframes的組合用來截取和記錄瀏覽器的曆史事件,來實現前進和回退的按鈕。這兩個技術都被用一個簡單的javascript庫來封裝,以利於開發人員的使用。
存在的問題
書籤和回退按鈕在傳統的多頁面的web應用上能順利的運行。當使用者在網站上衝浪時,他們的瀏覽器地址欄能更新URL,這些URL可以被粘貼到的email或者添加到書籤以備以後的使用。回退和前進按鈕也可以正常運行,這可以使使用者在他們訪問的頁面間移動。
Ajax應用是與眾不同的,然而,他也是在單一web頁面上成熟的程式。瀏覽器不是為Ajax而做的—Ajax他捕獲過去的事件,當web應用在每個滑鼠點擊時重新整理頁面。
在象Gmail那樣的Ajax軟體裡,瀏覽器的地址欄正確的停留就象使用者在選擇和改變應用的狀態時,這使得作書籤到特定的應用視圖裡變得不可能。此外,如果使用者按下了他們的回退按鈕去返回上一個操作,他們會驚奇的發現瀏覽器將完全離開原來他所在的應用的web頁面。
解決方案
開源的Really Simply History(RSH)架構解決了這些問題,他帶來了Ajax應用的作書籤和控制前進後退按鈕的功能。RSH目前還是beta版,在Firefox1.0上,Netscape7及以上,和IE6及以上運行。Safari現在還不支援。
目前存在的幾個Ajax架構可以協助我們做書籤和發布曆史,然而所有的架構都因為他們的實現而被幾個重要的bug困擾。此外,許多Ajax曆史框架組成綁定到較大的庫上,比如Backbase 和 Dojo,這些架構提供了與傳統Ajax應用不同的編程模型,強迫開發人員去採用一整套全新的方式去獲得瀏覽器的曆史相關的功能。
相應的,RSH是一個簡單的模型,能被包含在已經存在的Ajax系統中。而且,Really Simple History庫使用了一些技巧去避免影響到其他曆史架構的bug.
Really Simple History架構由2個javascript類庫組成,分別叫DhtmlHistory 和 HistoryStorage.
DhtmlHistory 類提供了一個對Ajax應用提取曆史的功能。Ajax頁面add() 曆史事件到瀏覽器裡,指定新的地址和關聯曆史資料。DhtmlHistory 類用一個錨的hash表更新瀏覽器現在的URL,比如#new-location ,然後用這個新的URL關聯曆史資料。Ajax應用註冊他們自己到曆史監聽器裡,然後當使用者用前進和後退按鈕導航的時候,曆史事件被激發,提供給瀏覽器新的地址和調用add()持續保留資料。
第二個類HistoryStorage,允許開發人員儲存任意大小的曆史資料。一般的頁面,當一個使用者導航到一個新的網站,瀏覽器會卸載和清除所有這個頁面的應用和javascript狀態資訊。如果使用者用回退按鈕返回過來了,所有的資料已經丟失了。HistoryStorage 類解決了這個問題,他有一個api 包含簡單的hashtable方法比如put(),get(),hasKey()。這些方法允許開發人員在離開web頁面時儲存任意大小的資料,當使用者點了回退按鈕返回時,資料可以通過HistoryStorage 類被訪問。我們通過一個隱藏的表單域(a hidden form field),利用瀏覽器即使在使用者離開web頁面也會自動儲存表單域值的這個特性,完成這個功能。
讓我們立即進入一個簡單的例子吧。
樣本1
首先,任何一個想使用Really Simple History架構的頁面必須包含(include)dhtmlHistory.js 指令碼。複製代碼 代碼如下:<!-- Load the Really Simple
History framework -->
<script type="text/javascript"
src="../../framework/dhtmlHistory.js">
</script>
DHTML History 應用也必須在和AJAX web頁面相同的目錄下包含一個叫blank.html 的指定檔案,這個檔案被Really Simple History架構綁定而且對IE來說是必需的。另一方面,RSH使用一個hidden iframe 來追蹤和加入IE曆史的改變,為了正確的執行功能,這個iframe需要指向一個真正的地址,不需要blank.html。
RSH架構建立了一個叫dhtmlHistory 的全域對象,作為操作瀏覽器曆史的入口。使用dhtmlHistory 的第一步需要在頁面載入後初始化這個對象。 複製代碼 代碼如下:window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
然後,開發人員使用dhtmlHistory.addListener()方法去訂閱曆史改變事件。這個方法擷取一個javascript回調方法,當一個DHTML曆史改變事件發生時他將收到2個自變數,新的頁面地址,和任何可選的而且可以被關聯到這個事件的曆史資料。 複製代碼 代碼如下:window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
// subscribe to DHTML history change
// events
dhtmlHistory.addListener(historyChange);
historyChange()方法是簡單易懂得,它是由一個使用者導航到一個新地址後收到的新地址(newLocation)和一個關聯到事件的可選的曆史資料historyData 構成的。複製代碼 代碼如下:/** Our callback to receive history change
events. */
function historyChange(newLocation,
historyData) {
debug("A history change has occurred: "
+ "newLocation="+newLocation
+ ", historyData="+historyData,
true);
}
上面用到的debug()方法是例子代碼中定義的一個工具函數,在完整的下載例子裡有。debug()方法簡單的在web頁面上打一條訊息,第2個Boolean變數,在代碼裡是true,控制一個新的debug訊息列印前是否要清除以前存在的所有訊息。
一個開發人員使用add()方法加入曆史事件。加入一個曆史事件包括根據曆史的改變指定一個新的地址,就像"edit:SomePage"標記, 還提供一個事件發生時可選的會被儲存到曆史資料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()方法被調用後,新地址立刻被作為一個錨值顯示在使用者的瀏覽器的URL欄裡。例如,一個AJAX web頁面停留在http://codinginparadise.org/my_ajax_app,調用了dhtmlHistory.add("helloworld", "Hello World Data" 後,使用者將在瀏覽器的URL欄裡看到下面的地址 複製代碼 代碼如下:http://codinginparadise.org/my_ajax_app#helloworld
然後他們可以把這個頁面做成書籤,如果他們使用這個書籤,你的AJAX應用可以讀出#helloworld值然後使用她去初始化web頁面。Hash裡的地址值被Really Simple History 架構顯式的編碼和解碼(URL encoded and decoded) (這是為瞭解決字元的編碼問題)
對當AJAX地址改變時儲存更多的複雜的狀態來說,historyData 比一個更容易的匹配一個URL的東西更有用。他是一個可選的值,可以是任何javascript類型,比如Number, String, 或者 Object 類型。有一個例子是用這個在一個多文字編輯器(rich text editor)儲存所有的文本,例如,如果使用者從這個頁面漂移(或者說從這個頁面導航到其他頁面,離開了這個頁面)走。當一個使用者再回到這個地址,瀏覽器會把這個對象返回給曆史改變接聽程式(history change listener)。
開發人員可以提供一個完全的historyData 的javascript對象,用嵌套的對象objects和排列arrays來描繪複雜的狀態。只要是JSON (JavaScript Object Notation) 允許的那麼在曆史資料裡就是允許的,包括單一資料型別和null型。DOM的對象和可程式化的瀏覽器對象比如XMLHttpRequest ,不會被儲存。注意historyData 不會被書籤持久化,如果瀏覽器關掉,或者瀏覽器的緩衝被清空,或者使用者清除曆史的時候,會消失掉。
使用dhtmlHistory 最後一步,是isFirstLoad() 方法。如果你導航到一個web頁面,再跳到一個不同的頁面,然後按下回退按鈕返回起始的網站,第一頁將完全重新裝載,並激發onload事件。這樣能產生破壞性,當代碼在第一次裝載時想要用某種方式初始化頁面的時候,不會再重新整理頁面。isFirstLoad() 方法讓區別是最開始第一次裝載頁面,還是相對的,在使用者導航回到他自己的瀏覽器曆史中記錄的網頁時激發load事件,成為可能。
在例子代碼中,我們只想在第一次頁面裝載的時候加入曆史事件,如果使用者在第一次裝載後,按回退按鈕返回頁面,我們就不想重新加入任何曆史事件。
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);
讓我們繼續使用historyStorage 類。類似dhtmlHistory ,historyStorage通過一個叫historyStorage的單一全域對象來顯示他的功能,這個對象有幾個方法來偽裝成一個hash table, 象put(keyName, keyValue), get(keyName), and hasKey(keyName).鍵名必須是字元,同時索引值可以是複雜的javascript對象或者甚至是xml格式的字元。在我們源碼source code的例子中,我們put() 簡單的XML 到historyStorage 在頁面第一次裝載時。
程式碼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);
}
然後,如果使用者從這個頁面漂移走(導航走)又通過返回按鈕返回了,我們可以用get()提出我們儲存的值或者用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() 是一個第一在例子源碼full example source code中的工具方法。這個方法準備簡單的xml顯示在web page ,方便調試。
注意資料只是在使用頁面的曆史時被持久化,如果瀏覽器關閉了,或者使用者開啟一個新的視窗又再次鍵入了ajax應用的地址,曆史資料對這些新的web頁面是停用。曆史資料只有在用前進或回退按鈕時才被持久化,而且在使用者關閉瀏覽器或清空緩衝的時候會消失掉。想真正的長時間的持久化,請看Ajax MAssive Storage System (AMASS).
我們的簡單樣本已經完成。示範他(Demo it)或者下載全部的原始碼(download the full source code.)
樣本2
我們的第2個例子是一個簡單的類比ajax email 應用的樣本,叫O'Reilly Mail,類似Gmail. O'Reilly Mail描述了怎樣使用dhtmlHistory類去控制瀏覽器的曆史,和怎樣使用historyStorage對象去緩衝曆史資料。
O'Reilly Mail 使用者介面(user interface)有兩部分。在頁面的左邊是一個有不同email檔案夾和選項的菜單,例如 收件匣,草稿,等等。當一個使用者選擇了一個功能表項目,比如收件匣,我們用這個功能表項目的內容更新右邊的頁面。在一個實際應用中,我們會遠程取得和顯示選擇的信箱內容,不過在O'Reilly Mail裡,我們簡單的顯示選擇的選項。
O'Reilly Mail使用Really Simple History 架構向瀏覽器歷史裡加入菜單變化和更新地址欄,允許使用者利用瀏覽器的回退和前進按鈕對應用做書籤和跳到上一個變化的菜單。
我們加入一個特別的功能表項目,地址簿,來描繪historyStorage 能夠怎樣被使用。地址簿是一個由聯絡的名字電子郵件和地址組成的javascript數組,在一個真實的應用裡我們會取得他從一個遠端伺服器。不過,在O'Reilly Mail裡,我們在本地建立這個數組,加入幾個名字電子郵件和地址,然後把他們儲存在historyStorage 對象裡。如果使用者離開了這個web頁面以後又返回的話,O'Reilly Mail應用重新從緩衝裡得到地址簿,勝過(不得不)再次訪問遠程伺服器。
地址簿是在我們的初始化initialize()方法裡儲存和重新取得的 程式碼/** 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");
}
處理曆史變化的代碼是簡單的。在下面的代碼中,當使用者不論按下回退還是前進按鈕handleHistoryChange 都被調用。我們得到新的地址(newLocation) 使用他更新我們的使用者介面來改變狀態,通過使用一個叫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的原始碼。
結束語
你現在已經學習了使用Really Simple History API 讓你的AJAX應用響應書籤和前進回退按鈕,而且有代碼可以作為建立你自己的應用的素材。我熱切地期待你利用書籤和曆史的支援完成你的AJAX創造。
著作權聲明:Techtarget獲Matrix授權發布,如需轉載請聯絡Matrix
作者: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
資源
·onjava.com:onjava.com
·Matrix-Java開發人員社區:http://www.matrix.org.cn/
·Download all sample code for this article.
http://www.onjava.com/onjava/2005/10/26/examples/downloads/examples.zip
·Download the Really Simple History framework.
http://codinginparadise.org/projects/dhtml_history/latest.zip
·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.
http://codinginparadise.org/
感謝
特別的要感謝每個檢閱這篇文章的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.