這篇文章描述了一個支持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現在還不支持(要得到更詳細的說明,請看我的weblog中的文章coding in paradise: safari: no dhtml history possible).
目前存在的幾個ajax框架可以幫助我們做書簽和發布歷史,然而所有的框架都因為他們的實現而被幾個重要的bug困擾(請看coding in paradise: ajax history libraries 得知詳情)。此外,許多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個自變量,新的頁面地址,和任何可選的而且可以被關聯到這個事件的歷史數據。
indow.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 '[email protected]'", "john doe '[email protected]'", "deanna neuberg '[email protected]'"]; // 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創造。