修补AJAX应用中Back/Forward Button和Bookmark失效的问题
想法与目标
于是我想,不如我来实现一个自己的吧,虽然我一直提倡软件复用,但是如果找不到成熟的解决方案,那么就该发挥程序员的主观能动性了。对于我最后的实现,它有以下特点:
对于我列出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值有没有改变。我的解决方案也不例外。
1
<%
@PageLanguage="C#"
%>
2 3 <! DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//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 > NavFix </ title > 12 < script language ="javascript" > 13varhistoryObj=null; 14varpreviousIndex=0; 15 16functiononChange(select) 17{ 18historyObj.addHistory(select.selectedIndex); 19} 20 21functionsetPageData(context) 22{ 23varselect=document.getElementById("select"); 24if(context) 25{ 26select.selectedIndex=context; 27varrequest=newSys.Net.WebRequest(); 28request.set_url("Selection.ashx?s="+select.selectedIndex); 29request.completed.add(onComplete); 30request.invoke(); 31} 32else 33{ 34select.selectedIndex=0; 35document.getElementById("message").innerHTML=""; 36} 37} 38 39functiononComplete(sender) 40{ 41document.getElementById("message").innerHTML=sender.get_data(); 42} 43 44functioninit() 45{ 46historyObj=newJeffz.Framework.History(setPageData,"NavFixHelper.htm"); 47historyObj.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:ScriptReferencePath="js/History.js"/> 57 </ Scripts > 58 </ atlas:ScriptManager > 59 60 < script type ="text/xml-script" > 61<pagexmlns:script="http://schemas.microsoft.com/xml-script/2005"> 62<components> 63<applicationload="init"/> 64</components> 65</page> 66 </ script > 67 68 < select onchange ="onChange(this)" id ="select" > 69 < option ></ option > 70 < option value ="1" > selection1 </ option > 71 < option value ="2" > selection2 </ option > 72 < option value ="3" > selection3 </ option > 73 </ select > 74 < div id ="message" style ="font-size:32px;" ></ div > 75 </ div > 76 </ form > 77 </ body > 78 </ html > 79 Selection.ashx
1
<%
@WebHandlerLanguage
=
"
C#
"
Class
=
"
Selection
"
%>
2 3 using System; 4 using System.Web; 5 6 public class Selection:IHttpHandler { 7 8publicvoidProcessRequest(HttpContextcontext){ 9stringvalue=String.Format( 10"Youselect:<strong>selection{0}</strong>", 11context.Request.QueryString["s"]); 12 13context.Response.Write(value); 14context.Response.End(); 15} 16 17publicboolIsReusable{ 18get{ 19returntrue; 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作为参数,将页面更新至之前的状态。
1
Jeffz.Framework.History
=
function
(setDataCallback,helperPageUrl)
2 { 3if(!setDataCallback||(typeofsetDataCallback!="function")) 4{ 5thrownewError("Pleaseprovideacallbackfunction"); 6} 7 8this.__setDataCallback=setDataCallback; 9this.__currentHash=null; 10this.__helperIFrame=null; 11this.__helperPageUrl=helperPageUrl; 12this.__runtimeTimer=null; 13 14if(Sys.Runtime.get_hostType()==Sys.HostType.InternetExplorer) 15{ 16if(!helperPageUrl) 17{ 18thrownewError("PleaseprovidethehelperpageforIE."); 19} 20else 21{ 22varhelperIFrame=document.createElement("iframe"); 23helperIFrame.style.display="none"; 24document.body.appendChild(helperIFrame); 25this.__helperIFrame=helperIFrame; 26this.__reloadHelperIFrame(location.hash); 27} 28} 29else 30{ 31this.__currentHash=location.hash; 32this.__execute(location.hash); 33} 34} 对于所有的私有成员,我都使用成员名前加上“__”的方法,加以区分。
1
Jeffz.Framework.History.prototype.__reloadHelperIFrame
=
function
(hash)
2 { 3this.__helperIFrame.document.title=document.title; 4 5if(this.__helperIFrame.src&&this.__helperIFrame.src.indexOf("?true")>=0) 6{ 7this.__helperIFrame.src=this.__helperPageUrl+"?false&"+document.title+hash; 8} 9else 10{ 11this.__helperIFrame.src=this.__helperPageUrl+"?true&"+document.title+hash; 12} 13} NavFixHelper.htm
1
<!
DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
2 < html xmlns ="http://www.w3.org/1999/xhtml" > 3 < head > 4 < title > UntitledPage </ title > 5 </ head > 6 < body > 7 < script language ="javascript" > 8window.parent.location.hash=location.hash; 9 10varqueryString=location.search; 11varindex=queryString.indexOf("&"); 12document.title=queryString.substring(index+1); 13 </ script > 14 </ body > 15 </ html > 把__reloadHelperIFrame函数和NavFixHelper.htm页面同时分析,是因为它们两个有密不可分的关系。它们都是仅仅为了IE服务的。
1
Jeffz.Framework.History.prototype.addHistory
=
function
(context)
2 { 3historyEntry={__p__:context}; 4 5if(Sys.Runtime.get_hostType()==Sys.HostType.InternetExplorer) 6{ 7this.__reloadHelperIFrame("#"+encodeURI(Sys.Serialization.JSON.serialize(historyEntry))); 8} 9else 10{ 11location.hash=encodeURI(Sys.Serialization.JSON.serialize(historyEntry)); 12} 13} 虽然可以说addHistory方法是关键,但是其实却非常的简单,在IE下则使用this.__reloadHelperIFrame来加载页面,否则就直接修改hash。hash的值使用的就是用户传入的context,序列化后,并经过encodeURI处理。
1
Jeffz.Framework.History.prototype.start
=
function
()
2 { 3if(!this.__runtimeTimer) 4{ 5vartimer=newSys.Timer(); 6timer.set_interval(300); 7timer.tick.add(this.__onTimerTick); 8timer.historyObj=this; 9this.__runtimeTimer=timer; 10} 11 12if(this.__runtimeTimer.get_enabled()) 13{ 14thrownewError("Thehistoryobjecthasbeenstarted."); 15} 16else 17{ 18this.__runtimeTimer.set_enabled(true); 19} 20} 21 22 Jeffz.Framework.History.prototype.stop = function () 23 { 24if(!this.__runtimeTimer||!this.__runtimerTimer.get_enabled()) 25{ 26thrownewError("Thehistoryobjecthasnotbeenstarted."); 27} 28else 29{ 30this.__runtimeTimer.set_enabled(false); 31} 32} 这两个函数非常简单,本来不用解释。唯一需要注意的是代码的第8行,由于Sys.Timer使用的是使用setTimerout来回调执行this.__onTimerTick,因此在this.__onTimerTick函数执行时,this并不是History对象本身,而是window!还好Sys.Timer会通过sender传递自身给回调函数,因此我在构造Sys.Timer时将自身对象放进了Timer对象的historyObj属性中,则在this.__onTimerTick函数中可以从sender.historyObj中得到History对象。
1
Jeffz.Framework.History.prototype.__onTimerTick
=
function
(sender,eventArgs)
2 { 3try 4{ 5if(location.hash!=sender.historyObj.__currentHash) 6{ 7sender.historyObj.__currentHash=location.hash; 8sender.historyObj.__execute(location.hash); 9} 10} 11catch(e) 12{ 13sender.set_enabled(false); 14sender.set_enabled(true); 15throwe; 16} 17} 其实这个方法就是比较hash值有没有改变,如果改变了,则保留新的hash,并加以执行回调函数(在__execute函数中)。可以发现,事实上回调函数this.__setDataCallbak总是在在onTimerTick里被执行的(除了在FireFox下构造History对象时),这样保证了this.__currentHash和当前页面hash的统一。
1
Jeffz.Framework.History.prototype.__execute
=
function
(hash)
2 { 3varhistoryEntry=null; 4 5try 6{ 7varhistoryEntry=Sys.Serialization.JSON.deserialize(decodeURI(hash.substring(1))); 8} 9catch(e) 10{ 11this.__setDataCallback(null); 12return; 13} 14 15if(historyEntry) 16{ 17this.__setDataCallback(historyEntry.__p__); 18} 19else 20{ 21this.__setDataCallback(null); 22} 23} 这段我想就不用多加解释了,似乎将一个简单的调用写得有些复杂。这样做的目的是在hash值是错误的情况下,会将null作为参数,保证了用户提供的回调函数被正确的使用。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |