这篇文章描述了一个支持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创造。