Ajax应用中浏览器历史的兼容性解决方案

Ajax技术的优势在于可以按需加载以及无刷新加载,用户可以在一个单独的页面中完成传统技术中需要多个页面间刷新、切换才能完成的操作,这使得用户体验大大提升,Ajax应用也越来越普及。不过,正是因为其“无刷新”的特点也留下了很多诟病,最严重的一点就是使得浏览器的“前进”、“后退”和“刷新”按钮失效,在操作的过程中,当用户因为某种原因希望撤销当前操作时,却发现曾经熟悉的“后退”按钮依然是灰色的,当用户按下“刷新”按钮后确发现刷新的不是当前的画面……等等。此外,当用户点完了页面上所有可以点的地方后终于发发现了一处有意思的地方,于是决定将有意思的东西发送给他的朋友,当他的朋友收到链接并打开后却没发现任何东西…..这就是在Ajax应用中普遍存在的另一个问题-动态内容的不可收藏性。

其实,在Ajax应用中是完全可以解决以上的“前进”、“后退”以及链接收藏问题的,但是鉴于各种浏览器的“特性”,需要解决很多问题才能实现这一目标,以下列出了实现浏览兼容历史管理方案需要解决的一些主要问题。

 

一、如何在Ajax应用中构建浏览器历史记录

1. 构建历史

在传统的网页应用中,当页面的URL发生变化后就会在浏览器的历史中留下一条记录,从而使得用户可以轻松的“后退”回去,但是在Ajax应用中,由于操作始终停留在当前页面,因此不允许修改页面的URL,取而代之的是修改URL中的hash,即location中的hash属性。hash原本是用于在当前页面进行导航的,当hash发生变化时可以在浏览器中留下历史记录而不会使得页面发生跳转或者刷新,这正符合了Ajax应用的特性,因此在大多数浏览器中的历史管理方案的实现中都是采用修改hash的方式来产生历史记录,包括IE8、Firefox、Safari、Opera和Chrome。然而目前市场份额最大的IE6和IE7除外,在IE6、7中修改hash并不会使得浏览器产生历史记录。

由于IE6、7中无法使用hash,于是只能采用性能更低的iframe。采用iframe是基于以下的事实:当iframe中的文档每次被重写后都会在浏览器中产生历史记录,当然使iframe中的文档重写的方式有很多种,例如修改iframe的src、直接打开iframe中的document进行重写等均可以在浏览器中留下历史记录。这一事实在Firefox以及Opera中也是成立的,不过不适用于Chrome和Safari。

IE实现中的iframe方案主要包括两种具体实现,这两种实现各有各的优缺点,具体选择依赖于实际应用的侧重点。

(1) Iframe Proxy

实现:Proxy是指Iframe在这种实现方案中的功能角色,即Iframe在这种方案中只扮演proxy的角色,不参与其他操作。这里所谓的proxy是指Iframe仅仅被借助于产生浏览器历史。在具体的实现中,是通过修改iframe中的document内容来产生浏览器历史,而当用户使用导航(前进、后退)时,Iframe中的内容会发生变化,而其父页面则通过监听Iframe中的内容来更新hash以及执行其他相应的操作。

优点:这种方式的优点在于不需要引入额外的iframe页面,而仅仅是增加了一个iframe元素,直接使用脚本来操作iframe的document。

缺点:与其优点相比,缺点似乎更明显,最大的问题在于当用户在页面中执行了“刷新”或者跨页面的导航(从Ajax应用页面前进或后退到别的页面)后,历史记录会发生丢失。此外,由于需要监听Iframe中的内容变化,需要增加一个额外的timer,不过这一点是可<以改良的,即在重写iframe document的时候直接在内容中加入父页面的回调函数即可省略这个timer了。该方式的示意图如图1.1所示

图1.1 IE中Iframe Proxy方案示意图

(2) Iframe Callback

实现:与proxy相比,该方案显得不是那么的轻便,因为需要增加一个额外的HTML文件作为iframe的内容。但增加这个页面却可以解决proxy方案中的缺点,因此也是非常值得的。这里的callback是指iframe在整个方案实现中充当的不仅仅是proxy(产生浏览器历史记录)而且也充当了proxy方案中timer的角色,即及时的通知父页面当前状态已经发生变化,而不需要父页面额外的增加timer。Proxy方案中采用了直接修改iframe document的方法来产生浏览器历史记录,而该方案是通过修改iframe的src属性实现的,具体的方式是在其src之后增加查询串,类似blank.html?action=xxx。修改src的查询串可以迫使iframe中的内容重新加载,从而可以产生历史记录。此外,当用户进行导航操作时,iframe页面也会重新加载,此时iframe可以及时的把变化通知给父页面。

优点:可以解决Proxy方案中的问题。

缺点:需要增加额外的HTML页面,不如Proxy方案轻便。该方式的示意图如图1.2所示

图1.2 IE中Iframe Callback方案示意图

2. 监听历史变更

当用户进行导航操作时,需要及时的监控到当前历史状态。而在当前的主流浏览器中均未提供相关的事件机制可以监听hash的变化,这就需要我们自己通过额外的编码来解决。IE8在这方面往前多跨了一步,不仅仅添加了通过修改hash可以增加历史记录的功能,还添加了hash变化的事件,因此在IE8中只需要注册相关的事件即可。在此主要讨论除IE8之外的浏览器下的解决方案。

(1)Firefox、Safari、Opera、Chrome

这类浏览器均是通过修改hash来产生历史记录的,用户在导航的时候hash也会不断的变化,此时可以设置一个timer来监听这种变化。不过需要注意的一个问题是,在Opera下,当用户进行了跨页面导航后重新返回页面时,原先的timer会发生丢失,导致无法继续监控,不过这是有相应的trick来解决的,在页面中添加一个img标签,并设置其src为javascript:location.href=’javascript:checkStateChange()即可。

(2)IE6、IE7

IE6、IE7下也可以采用timer的方式,不过监听的不是hash的变化(因为导航时IE6、7下的hash是不会自己变的),而是监听iframe中的内容变化。此外,也可以采用callback方法,即在iframe中的内容加载的时候调用父页面的方法进行通知即可。

(3)IE8

IE8中可以直接注册window上的onhashchange事件,当hash发生变化时该事件会触发。

二、如何解决Ajax应用在跨页面间的历史记录问题

在具体的实现中,需要提供一种数据存储的方案,即针对每一个state都可以保存一个数据,当导航到该state时可以将数据重新传递给监听器。在Javascript中,可以使用一个object来实现,以state为Key,数据为Value。这一实现在导航仅限于当前页面的情况下是没有问题的,但当用户一旦导航到别的页面再导航回来时,这些object数据均会丢失。在页面间导航时能被记录下来的是表单元素中的值,例如在B页面中的textarea中填写了东西然后后退到A页面,再从A页面前进到B页面时会发现textarea中的值依然存在,经测试,这一现象在各个浏览器中均保持了一致性,正因为此,可以使用该trick来解决object丢失的问题。

当用户添加历史记录时首先从表单元素(textarea)中读取内容并反序列化成Javascript Object,之后将数据添加到object中并序列化成字符串后保存到textarea中。

除了数据丢失问题,还需要注意Opera下的timer丢失问题(上文已述)。

三、如何识别Ajax应用的新旧加载

除了以上几点,方案的实现中还需要注意的一个细节是对页面新旧加载的判断。设想有这样一个应用,当用户第一次进入页面时会给一个短暂的欢迎和Loading的窗口,并在资源加载完成后消失。用户在几步操作后页面跳转到了新的页面,之后后退回原页面时是不应该再出现那个欢迎窗口的,要实现这一功能就必须能够识别页面是第一次进入(在地址栏输入链接、或者点击链接)还是从别的页面导航进入的。这需要有跨页面间的数据记忆能力,与问题二一样,这里也采用了表单元素。第一次进入页面后会在表单元素中设置数据,在每次页面加载的时候都会检查该元素的内容来得到结果。

在这一方案的具体实现过程中,发现在Safari和Opera下,当页面是从别的页面导航过来后打开的则其中的脚本是不会执行的。这原本对上述方案是致命的,不过幸运的,虽然脚本不会继续执行,但是页面原本的DOM结构是可以被记录的,这样就可以保证页面在导航出和导航入的时候能否保持一致。

四、具体实现

请参考Demo以及具体的JS实现

JS从这里下载

注:

  1. 以上脚本经测试可正常导航的包括:IE6、IE7、Firefox3.0+、Opera9.6、Safari3.0+。
  2. 首次进入页面的检测功能在IE以及FIrefox下能正常使用(包括跨页面导航以及非强制刷新),但是在Chrome、Opera和Safari下由于刷新会导致表单数据丢失因此会被当成首次加载,不过跨页面间的检测正常。
  3. 由于在Chrome、Opera和Safari下刷新会导致表单数据丢失,因此不要将hash与数据绑定,否则跨页面和刷新均会丢失。

五、参考资料

《JerryQu:如何控制浏览器的历史记录》
《YUI Library: Browser History Manager》

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

3 Comments »

 
  • JK 说:

    考虑得真全面啊

  • 也研究过,但是远远不及你的详细…
    javascript:location.href=’javascript:checkStateChange()很好

    原来Chrome和Safari是不能用iframe的
    鄙人记得chrome的某个版本更改hash不会产生历史记录…这样是不是就挂了…

  • […] 3. 经JK提醒,完成以上两步还有一个问题,当用户离开当前页面之后后退回页面时,时间计算不准确。问题在于基准时间是服务器给的,在第一次进入页面的时候确定,当用户后退回当前页面时,基准时间并没有变,这样会导致重新从过期的基准时间开始计算,导致不准确。需要解决这个问题就是需要解决跨页面的数据存储问题,这在之前的《Ajax应用中浏览器历史的兼容性解决方案》一文中已经说明,即通过表单元素来记忆。具体的实现方案是,页面第一次加载时创建两个input,一个用于存储最近一次的客户端时间,一个用于存储最近一次的基准时间。如果发现已经存在input(前进、后退、非强制刷新)则比较上一次的客户端时间与当前客户端时间,如果其差值大于某个预设值则像步骤2中一样进行校准,只不过使用的将是最新的基准值。 […]

 

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">