ajax|按鈕|書籤
本文將展示一個開源JavaScript庫,該指令碼庫給AJAX應用程式帶來了書籤和後退按鈕支援。在學習完這個教程後,開發人員將能夠獲得對一個AJAX問題的解決方案(甚至連Google Maps和Gmail現在都不提供該解決方案):一個強大的、可用的書籤和後退前進功能,其操作行為如同其他的Web應用程式一樣。
簡單樣本下載完整原始碼。
樣本二下載O'Reilly Mail原始碼。
本文將闡述目前AJAX應用程式在使用書籤和後退按鈕方面所面臨的嚴重問題;展示Really Simple History(RSH)庫——一個可以解決以上問題的開源架構,並提供幾個運行中的例子。
本文所展示的這個架構的主要發明分為兩部分。首先是一個隱藏的HTML表單,用於緩衝大量短期會話的用戶端資訊;這種緩衝功能為頁面導航提供了強大的支援。其次是超連結錨點和隱藏Iframe的組合,它們被嵌入後退和前進按鈕,用來截獲和記錄瀏覽器的記錄事件。以上兩種技術都被封裝在一個簡單的JavaScript庫中來簡化開發。
問題
書籤和後退按鈕在傳統的多頁面web應用程式中運行得非常好。當使用者瀏覽web網站的時候,其瀏覽器的地址欄記錄隨新的URL而更新,這些記錄可以被粘貼到電子郵件或者書籤中供以後使用。後退和前進按鈕也可以正常操作,使使用者可以在訪問過的頁面中向前或向後翻動。
但是AJAX應用程式卻不一樣,它們是運行在單個web頁面中的複雜程式。瀏覽器並不是為這類程式而構建的——這類web應用程式已經過時,它們在每次滑鼠點擊的時候都需要重新重新整理整個頁面。
在這種類似於Gmail的AJAX軟體中,瀏覽器的地址欄在使用者選擇功能和改變程式狀態的時候保持不變,這使得無法在特定的應用程式視圖中使用書籤。此外,如果使用者按下[上一頁] 按鈕來“撤銷”上次的操作,他們會驚奇地發現,瀏覽器會完全離開該應用程式的web頁面。
解決方案
開源RSH架構可以解決這些問題,它為AJAX應用程式提供了書籤和控制後退、前進按鈕的功能。RSH目前還處於Beta階段,可以在Firefox 1.0、Netscape 7+、Internet Explorer 6+等瀏覽器上運行;目前還不支援Safari。
目前有幾個AJAX架構對書籤和記錄問題有所協助;但這些架構目前都有幾個由於實現而造成的重大Bug。此外,很多AJAX記錄架構被綁定到較大的庫上,例如Backbase和Dojo;這些架構為AJAX應用程式引入了完全不同的編程模型,迫使開發人員使用全新的方式來獲得記錄功能。
相較之下,RSH是一個可以包含在現有AJAX系統中的簡單模組。此外,RSH庫採用了一些技術以避免產生影響其他記錄架構的Bug。
RSH架構由兩個javaScript類組成:DhtmlHistory和HistoryStorage。
DhtmlHistory類為AJAX應用程式提供記錄抽象。AJAX頁面使用add()方法添加記錄事件到瀏覽器,指定新的地址和相關的記錄資料。DhtmlHistory類使用一個錨散列(如#new-location)更新瀏覽器當前的URL,同時把記錄資料和該新URL關聯。AJAX應用程式將自己註冊為記錄的監聽器,當使用者使用後退和前進按鈕進行瀏覽時,記錄事件被觸發,為瀏覽器提供新的位置以及與add()調用一起儲存的任何記錄資料。
第二個類:HistoryStorage,允許開發人員儲存任意數量的已存記錄資料。在普通web頁面中,當使用者導航到一個新的web網站時,瀏覽器卸載並清除web頁面上的所有應用程式和javaScript狀態;如果使用者用後退按鈕返回,所有的資料都丟失了。HistoryStorage類通過一個包含簡單散列表方法(例如put()、get()、hasKey())的API來解決這類問題。上面的方法允許開發人員在使用者離開web頁面之後儲存任意數量的資料;當使用者按後退按鈕重新返回時,記錄資料可以通過HistoryStorage類來訪問。在內部,我們通過使用隱藏的表單欄位來實現此功能,這是因為瀏覽器會自動儲存表單欄位中的值,甚至在使用者離開web頁面的時候也如此。
例子
讓我們先從一個簡單的例子開始。
首先,任何需要使用RSH架構的頁面都必須包含dhtmlHistory.js指令碼:
<!-- Load the Really Simple
History Framework -->
<script type="text/javascript"
src="../../framework/dhtmlHistory.js">
</script>
DHTML記錄應用程式也必須在與AJAX web頁面相同的目錄下包含blank.html檔案;這個檔案與RSH架構打包在一起,且對於Internet Explorer來說是必需的。順便提一下,RSH使用一個隱藏Iframe來跟蹤和添加Internet Explorer的記錄變化;這個Iframe需要我們指定一個實際的檔案位置才能正常工作,這就是blank.html。
RSH架構建立了一個叫做dhtmlHistory的全域對象,這是操縱瀏覽器記錄的進入點。使用dhtmlHistory的第一步是在web頁面載入完成後初始化dhtmlHistory對象:
window.onload = initialize;
function initialize() {
// initialize the DHTML History
// framework
dhtmlHistory.initialize();
然後,開發人員使用dhtmlHistory.addListener()方法訂閱記錄變化事件。這個方法帶有一個javaScript回呼函數,當DHTML記錄變化事件發生時,該函數接收兩個參數:新的頁面位置以及任何可與該事件關聯的可選記錄資料:
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頁面上;第二個布爾型參數(在上述代碼中值為true)控制是否在列印新的調試訊息之前清除原有的全部訊息。
開發人員使用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地址欄中。例如,對地址為http://codinginparadise.org/my_ajax_app的AJAX web頁面調用dhtmlHistory.add("helloworld", "Hello World Data")之後,使用者將會在其瀏覽器URL地址欄中看到如下的地址:
http://codinginparadise.org/my_ajax_app#helloworld
然後使用者可以將這個頁面做成書籤,如果以後用到這個書籤,AJAX應用程式可以讀取#helloworld值,並用它來初始化web頁面。散列後面的地址值是RSH架構可以透明編碼和解碼的URL地址。
HistoryData非常有用,它儲存比簡單的URL更為複雜的AJAX地址變化狀態。這是一個可選值,可以是任何JavaScript類型,例如Number、String或Object。使用該儲存功能的一個例子是在一個富文字編輯器中儲存所有文本(比如在使用者離開當前頁面時)。當使用者再回到這個地址時,瀏覽器將會將該對象返回給記錄變化監聽器。
開發人員可以為historyData提供帶有嵌套對象和表示複雜狀態的數組的完整JavaScript對象;JSON (javaScript Object Notation)所支援的在記錄資料中都支援,包括單一資料型別和null類型。然而,DOM對象以及可用指令碼編寫的瀏覽器對象(如XMLHttpRequest)不會被儲存。請注意,historyData並不隨書籤一起儲存,當瀏覽器關閉,瀏覽器緩衝被清空,或者使用者清除記錄的時候,它就會消失。
使用dhtmlHistory的最後一步是isFirstLoad()方法。在某些瀏覽器中,如果導航到一個Web頁面,再跳轉到另一個不同的頁面,然後按[上一頁] 按鈕返回到起始的網站,第一頁將完全重新載入,並觸發onload事件。這樣會對想要在第一次載入頁面時用某種方式對其進行初始化(而其後則不使用這種方式重新載入該頁面)的代碼造成破壞。isFirstLoad()方法可以區分是第一次載入一個web頁面還是使用者導航到儲存在記錄中的web頁面時觸發的“假載入”事件。
在範例程式碼中,我們只想在第一次載入頁面的時候添加記錄事件;如果使用者在載入頁面後按後退按鈕返回該頁面,我們就不想重新添加任何記錄事件:
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的全域對象來公開它的功能。該對象有幾個類比散列的方法,比如put(keyName、keyValue)、get(keyName)和hasKey(keyName)。鍵名稱必須是字串,同時索引值可以是複雜的javaScript對象甚至是XML格式的字串。在我們的原始碼例子中,在第一次載入頁面時,我們使用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()是一個定義在完整樣本原始碼中的實用方法;此函數準備在web頁面中顯示以便用於調試的簡單XML。
請注意,相關資料只在該頁面的記錄中進行持久化;如果瀏覽器被關閉,或者使用者開啟一個新視窗並再次鍵入AJAX應用程式的地址,則該記錄資料對於新的web頁面不可用。記錄資料只有在用到後退或前進按鈕時才被持久化,當使用者關閉瀏覽器或清空緩衝的時候就會消失。如果想真正長期持久化,請參閱Ajax MAssive Storage System (AMASS)。
樣本2:O'Reilly Mail
我們的第二個例子是一個簡單的AJAX電子郵件類比應用程式的樣本,即O'Reilly Mail,它類似於Gmail。O'Reilly Mail描述了如何使用dhtmlHistory類來控制瀏覽器的記錄,以及如何使用historyStorage對象來緩衝記錄資料。
O'Reilly Mail使用者介面由兩部分組成。在頁面的左邊是一個帶有不同電子郵件檔案夾和選項的菜單,例如收件匣、草稿箱等。當使用者選擇了一個功能表項目(如收件匣),就用這個功能表項目的內容更新右邊的頁面。在一個現實應用程式中,我們會遠程擷取並顯示選擇的信箱內容,不過在O'Reilly Mail中,我們只顯示已選擇的選項。
O'Reilly Mail使用RSH架構向瀏覽器記錄中添加菜單變化並更新地址欄,允許使用者利用瀏覽器的後退和前進按鈕為應用程式做收藏書籤和跳到上次變化的菜單。
我們添加一個特殊的功能表項目——地址簿,以說明如何來使用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。使用O'Reilly Mail定義的displayLocation實用方法,我們可得到newLocation並使用它將我們的使用者介面更新到正確的狀態。
/** 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;
}