起初打算使用 document.referrer 来判断,后来经过实际测试,发现在“后退”场景下 document.referrer 不能返回正确的结果。比如,A 页面跳转到 B 页面,B 页面的 document.referrer 为 A;A 页面跳转到 B 页面跳转到 C 页面,再后退到 B 页面,这时查看 B 页面的 document.referrer,发现还是 A ....恩,这条路是走不通的。
随后考虑根据浏览器的一个特性——“后退时 URL 不会改变”来实现判断。这个特性是:如果你从 A URL 跳转到了 B 页面,那么点击后退,再次访问的一定是同样的 A URL。在用户点击链接,跳转离开页面之前(比如写在 unload 事件里),改变 URL,如将 A 变成 A#token,这样从 B 页面后退时,访问的就是 A#token;在 JS 中再加一段逻辑,判断 token 是否存在,即可得知是否是后退进入。
title: 微信浏览器实现后退记忆浏览位置 categories:
historyBack toc: true date: 2016-08-09 15:00:11
不知道你有没有注意过,当点击浏览器工具栏上的后退按钮,亦或是在移动设备上触发浏览器的后退键,浏览器会在当前窗口打开前一个页面,不同浏览器“打开”前一个页面的方式是不同的。chrome 浏览器点击后退按钮时,会准确返回到前一个页面离开时的状态,连 input 中输入的字都会还原,而微信浏览器的行为则大不一样。
背景
当触发浏览器的后退按钮时,用户希望页面能返回到与上次离开时分毫不差的状态,这样便可继续浏览与操作;然而这并不是所有浏览器都能做到的。在移动端对浏览器做测试,结果如下:
对于静态页面,在微信浏览器中点击后退,可以返回到离开时浏览位置。然而对于带有下拉加载的页面,微信浏览器就做不到了:微信浏览器要重新执行 js,页面要重新渲染。举个例子:离开页面时可能下滑加载了好几页,offsetTop 到了10000多,点击后退后,页面重新渲染,回到了初始状态,页面总高度可能也就6000,这时微信浏览器尝试回到 offsetTop 10000,只能回到页面最底部;往往这时又会触发下滑加载的逻辑,页面会再吐出一些数据,体验就很差劲了。
相比之下,chrome 和 safari 因为具有 BFcache,页面前进时会把页面状态完整保存在内存里,后退时直接取缓存,不用做任何操作,即可完美实现“后退时返回到上次浏览位置”。
调查了一下其他大型网站,如京东、今日头条、饿了么、腾讯新闻等,都没有对微信浏览器中的这个问题做出修复;我司大部分流量来自于微信浏览器,这个问题一定要克服,所以就需要自己写逻辑了,让微信浏览器也能实现后退时准确返回到上次浏览位置。
思路
将问题简化一下,则为“后退时,使上一个页面准确返回到上次浏览位置”。由于JS要重新执行,所以可以在 JS 中加两部分的逻辑:一是在页面前进时,将当前状态缓存下来;二是后退到该页面时,取出状态并手动复原。
这种方案的难点有两个:
判断页面来源
起初打算使用 document.referrer 来判断,后来经过实际测试,发现在“后退”场景下 document.referrer 不能返回正确的结果。比如,A 页面跳转到 B 页面,B 页面的 document.referrer 为 A;A 页面跳转到 B 页面跳转到 C 页面,再后退到 B 页面,这时查看 B 页面的 document.referrer,发现还是 A ....恩,这条路是走不通的。
随后考虑根据浏览器的一个特性——“后退时 URL 不会改变”来实现判断。这个特性是:如果你从 A URL 跳转到了 B 页面,那么点击后退,再次访问的一定是同样的 A URL。在用户点击链接,跳转离开页面之前(比如写在 unload 事件里),改变 URL,如将 A 变成 A#token,这样从 B 页面后退时,访问的就是 A#token;在 JS 中再加一段逻辑,判断 token 是否存在,即可得知是否是后退进入。
经过测试,这种方法很稳定,但是改变 hash 有两个不可忽视的缺点:一个 URL 只能有一个 hash,如果页面的业务逻辑代码需要操作 hash,那么很容易出现冲突的现象。二是修改 hash 会将当前 url 推入 history 栈,那么用户在连续后退时会出现这种情况:B 页面 --> A#token --> A,A#token 状态下点击后退到 A,用户会觉得“咦这次后退怎么没反应”,体验也不是很好。
再次思考,除了 hash 以外改变URL的方式——那就是html5中新出的historyAPI了。通过history.replace这个API,可以方便的修改URL,同时还不会影响history栈,简直完美。可惜查看兼容性:ios兼容良好,安卓4.1+开始支持historyAPI。
虽然安卓4.1-的用户已经很少了,但是能解决的问题就要尽量解决。不能用historyAPI,还有别的办法吗?这时,我注意到了安卓微信浏览器的一个特性:后退时不会重新请求页面本体。利用这个特点,可以将token整合在服务器端返回的html中。由于我做的项目前后端中间存在一个PHP层,那么使用php在页面中打入一个时间戳timestamp;每次执行JS时,获取页面上的时间戳,并试图与缓存中的时间戳进行比较(若缓存中无时间戳则将本次获得的时间戳写入);若为后退进入,则比对结果则为相同;若非后退进入,则获取到的时间戳必大于缓存中保存的时间戳。
如何缓存
我们要实现的是“缓存状态并还原”,说起来容易做起来难。比如这个页面有个下滑加载,当前加载到第3页了,那么当你后退回来时,不但页面样式要立刻还原为第3页的状态,还要保证接下来下滑出来的是第4页,而不是第1页。
由于项目中模块化做的不错,每个页面基本都是一个独立的模块,并且模块中一般划分为init、render(构建并渲染HTML字符串)、bindEvent(绑定事件)几部分,相关变量均为模块的私有参数。所以使用了简单粗暴的方式:直接将DOM结构与需要保存的状态参数(例如当前看到第几页)缓存入sessionStorage中。页面init时会检测是否为后退进入,非后退进入则依次调用render、bindEvent,后退进入则不再调用render,直接取缓存的DOM填入页面,并将上下文变量还原,再执行一遍bindEvent,最后调用scrollTo,将滚动条定位到离开时浏览到的位置。
思路
离开页面时:
进入页面时:
tips
beforeunload事件被微信浏览器阉割掉了,完全不能使用。
参考