修补AJAX应用中Back/Forward Button和Bookmark失效的问题
2006-09-14 19:48:11
版权声明:原创作品,如需转载,请与作者联系。否则将追究法律责任。 |
想法与目标 从AJAX诞生至今,就存在着Back/Forward Button和bookmark失效的问题,我以前一般提倡,一个好的AJAX应用应该不让用户有点击“Back/Forward”的想法,并且使用某种 方式提供给用户一个能够记录直接产生页面的Bookmark。Windows Live Local应该是这种应用最好的典范之一,其灵活的交互,良好的界面让我在初遇时不得不眼前一亮。 另外,我也曾经见过把后退按钮禁用的做法(其实这样对于解决问题的确不错),不过这些都似乎只是一个workaround,设法避开这个AJAX应用普 遍存在的问题。似乎Gmail能够支持Back按钮,但是我惊奇的发现,在点击Back后,却不能使用Forward,所以这还不算成功的解决这个问题。 那么能否解决?似乎已经有了一定的实现。 事实上,之所以我会产生实现自己的解决方案的想法,是因为从Nikhil Kothari的Blog上看到了他的解决方案(点击这里查看)。 他实现了一个HistoryControl控件,可以在页面中配合UpdatePanel使用,在一定程度上实现了对Back/Forword Button已经Bookmark的支持。但是正如他在Blog上所说的,这只是他的一个prototype。我在使用了他的演示之后,也的确发现了一些 问题(演示也能从Nikhil的Blog上下载):
于是我想,不如我来实现一个自己的吧,虽然我一直提倡软件复用,但是如果找不到成熟的解决方案,那么就该发挥程序员的主观能动性了。对于我最后的实现,它有以下特点:
对于我列出Nikhil的实现里的第5个问题,我想了一些办法,却依旧没有解决。现在虽然脑子里有想法,但还需要继续尝试。
思路与设计 AJAX是个神奇的东西。因为有了XMLHttpRequest对象,我们能够“不知不觉”地与服务器端交换数据,在改变页面显示和行为的同时,让用户 感觉不到页面的Reload。虽然在上个世纪微软已经在早期IE里就以ActiveX的形式提供了这个对象,并且在OWA中将其进行大量使用,但是我对于 这个对象的了解却在AJAX大行其道之后。在这之前,为了达到类似AJAX的良好用户体验,往往会在页面中放一个隐藏的IFrame,然后通过Form向 其中POST/GET数据,或者直接修改IFrame的src属性以达到传输数据的效果。如果在IFrame中的页面里写JS代码,就能通过 window.parent.XXX来调用父级页面的对象或方法,并可以访问整个DOM(当然跨Frame操作的话需要两张页面在同一个Domain中, 这个就是IFrame sandbox,如果了解Windows Live Gadget和Windows Live Spaces Gadget的人就能体会到在安全性方便IFrame起的重要作用)。 当时发现,只要改变IFrame里的地址,不论是 POST/GET还是改变其src属性,大都会在浏览器的History里留下痕迹。这是如果用户点击浏览器的Back按钮,则会从IFrame里的 History里Load以前的页面,当然也会按照那张页面的逻辑解释执行其中的JS代码。善于利用这点的话,就会产生父页面Back/Forward的 效果。可惜当时没有去想这一点,而且当时因为某些问题,用户点击Back/Forward时反而会产生异常的行为,甚是麻烦。 与 POST/GET相比,改变IFrame的src属性相对简单,也容易操作。但是在IE重要注意的是,并不是任意修改src时都会使IFrame被加入 History。修改src的时候其实改变了IFrame里的location。location是window的一个属性,它分几个部分,这里需要提到 的就是它的href,search和hash。举个例子,对于一个location“http://www.sample.com?a=b&c= d#hello”来说,location.href是“http://www.sample.com”,location.search是“?a= b&c=d”(可以看出,search其实就是Query String),location.hash是“#hello”。在IE中改变location.hash是不会影响History的,因此只有改变 href与search才行。在FireFox中,改变hash值是可以影响浏览器的History,但是点击Back/Forward并不会使浏览器重 新执行页面中的JS代码。 解决Back/Forward的大致方向有了,那么Bookmark呢?应该很容易将问题变成,如何要改变浏览器地址栏的值,但是不刷新页面。还好我们有hash。所有的标识都要通过hash值来传递。 到现在为止,应该已经能够实现了在不刷新页面时改变浏览器的History纪录,但是如何在用户点击Back/Forward的时候也改变页面内容呢?我们将这个问题分为两部分考虑,依次解决:
对于第1个问题,在FireFox下很容易解决,因为其实这是浏览器已经支持的功能。如果是IE,我们只能通过IFrame里的页面来改变父窗口的
hash了。第2个问题比较麻烦,因为改变浏览器的hash值并不会触发一个事件,所以迄今为止似乎所有的解决方案都是使用timer来不停地查询
hash值有没有改变。我的解决方案也不例外。
解释完了思路与设计,就该转向真正的实现了吧。 实现与分析 在分析实现之前,可以先点击这里下载我的代码,或者点击这里查看例子。我实现了一个类“Jeffz.Framework.History”。先从使用讲起。 Default.aspx 1
<%@ Page Language="C#" %>2 ![]() 3 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">4 ![]() 5 <script runat="server">6 ![]() 7 </script>8 ![]() 9 <html xmlns="http://www.w3.org/1999/xhtml" >10 <head runat="server">11 <title>Nav Fix</title>12 <script language="javascript">13 var historyObj = null;14 var previousIndex = 0;15 16 function onChange(select)17 {18 historyObj.addHistory(select.selectedIndex);19 }20 21 function setPageData(context)22 {23 var select = document.getElementById("select");24 if (context)25 {26 select.selectedIndex = context;27 var request = new Sys.Net.WebRequest();28 request.set_url("Selection.ashx?s=" + select.selectedIndex);29 request.completed.add(onComplete);30 request.invoke();31 }32 else33 {34 select.selectedIndex = 0;35 document.getElementById("message").innerHTML = "";36 }37 }38 39 function onComplete(sender)40 {41 document.getElementById("message").innerHTML = sender.get_data();42 }43 44 function init()45 {46 historyObj = new Jeffz.Framework.History(setPageData, "NavFixHelper.htm");47 historyObj.start();48 }49 </script>50 </head>51 <body style="font-family: Arial;">52 <form id="form1" runat="server">53 <div>54 <atlas:ScriptManager ID="ScriptManager1" runat="server" EnableScriptComponents="true">55 <Scripts>56 <atlas:ScriptReference Path="js/History.js" />57 </Scripts>58 </atlas:ScriptManager>59 60 <script type="text/xml-script">61 <page xmlns:script="http://schemas.microsoft.com/xml-script/2005">62 <components>63 <application load="init" />64 </components>65 </page>66 </script>67 ![]() 68 <select onchange="onChange(this)" id="select">69 <option></option>70 <option value="1">selection 1</option>71 <option value="2">selection 2</option>72 <option value="3">selection 3</option>73 </select>74 <div id="message" style="font-size: 32px;"></div>75 </div>76 </form>77 </body>78 </html>79 ![]() Selection.ashx 1
<%@ WebHandler Language="C#" Class="Selection" %>2 ![]() 3 using System;4 using System.Web;5 ![]() 6 public class Selection : IHttpHandler {7 8 public void ProcessRequest (HttpContext context) {9 string value = String.Format(10 "You select: <strong>selection {0}</strong>", 11 context.Request.QueryString["s"]);12 13 context.Response.Write(value);14 context.Response.End();15 }16 17 public bool IsReusable {18 get {19 return true;20 }21 }22 }
首先Application在Load之后会立即调用init方法,构造一个Jeffz.Framework.History对象
historyObj,需要传入更新数据的回调函数,还有为了IE单独提供的NavFixHelper.htm文件的路径。然后调用对象的start方法
开启对于hash值的监听。对象还提供了一个stop来停止监听。我提供这两个方法的目的是为了能够在需要时停止timer,方便调试。需要更新页面内容
时,如果要保留History,那么必须通过historyObj的addHistory来提供修改所需要使用的参数context。context可以
为任意对象,将会被序列化之后被放置在地址栏的hash中,然后在构造historyObj时传入的回调函数(setPageData)会被执行,
context会被作为参数传入回调函数。使用historyObj时,对于页面的修改都应该放在setPageData中。在用户通过点击
Back/Forward
Button或者直接选择History的某一项时,地址栏中的hash会改变,回调函数会获得从当前hash得到的context作为参数,将页面更新
至之前的状态。
在setPageData被调用时,<select />的选项会被更改,然后会使用Sys.Net.WebRequest向Selection.ashx发送请求。Selection.ashx根据 Query String的值来返回信息。然后Sys.Net.WebRequest对象在收到response后修改message的信息。 在使用上就是这么简单。 History.js - Constructor 1
Jeffz.Framework.History = function(setDataCallback, helperPageUrl)2 {3 if (!setDataCallback || (typeof setDataCallback != "function"))4 {5 throw new Error("Please provide a callback function");6 }7 ![]() 8 this.__setDataCallback = setDataCallback;9 this.__currentHash = null;10 this.__helperIFrame = null;11 this.__helperPageUrl = helperPageUrl;12 this.__runtimeTimer = null;13 14 if (Sys.Runtime.get_hostType() == Sys.HostType.InternetExplorer)15 {16 if (!helperPageUrl)17 {18 throw new Error("Please provide the helper page for IE.");19 }20 else21 {22 var helperIFrame = document.createElement("iframe");23 helperIFrame.style.display = "none";24 document.body.appendChild(helperIFrame);25 this.__helperIFrame = helperIFrame;26 this.__reloadHelperIFrame(location.hash);27 }28 }29 else30 {31 this.__currentHash = location.hash;32 this.__execute(location.hash);33 }34 } 对于所有的私有成员,我都使用成员名前加上“__”的方法,加以区分。
首先,传入的第一个参数setDataCallback必须是一个函数,否则将抛出异常。紧接着判断浏览器类型,如果是IE,则检测必须提供 helperPageUrl,并且构造一个隐藏的IFrame来辅助实现history功能,最后通过 this.__reloadHelperIFrame方法来修改地址栏里的hash。如果不是IE,那么就直接将this.__currentHash设 成当前的hash加以记录,并调用this.__execute函数将hash值构造成context对象,并执行回调函数 this.__setDataCallback。 History.js - __reloadHelperIFrame 1
Jeffz.Framework.History.prototype.__reloadHelperIFrame = function(hash)2 {3 this.__helperIFrame.document.title = document.title;4 ![]() 5 if (this.__helperIFrame.src && this.__helperIFrame.src.indexOf("?true") >= 0)6 {7 this.__helperIFrame.src = this.__helperPageUrl + "?false&" + document.title + hash;8 }9 else10 {11 this.__helperIFrame.src = this.__helperPageUrl + "?true&" + document.title + hash;12 }13 }NavFixHelper.htm
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">2 <html xmlns="http://www.w3.org/1999/xhtml" >3 <head>4 <title>Untitled Page</title>5 </head> |









}