解决浏览器返回页面不刷新的问题

23,783 阅读8分钟

现象

由于 IOS 系统的页面缓存机制,经常会遇到在移动端返回到上一个页面不刷新的情况。

比如今天在开发微信 H5 页面的时候,在IOS微信内置浏览器中返回上一页时,上一个页面不会被刷新。 而通常在浏览器缓存机制中,在返回上一页的操作中, html/css/js/接口 等动静态资源不会重新请求,但是js会重新加载。但在IOS微信页面中js也会保存上一页面最后执行的状态,不会重新执行js。 使用这种模式的缓存机制可以加快渲染速度,但是部分数据需要经常展示和编辑的情况下会导致不同步。比如‘详情页’跳转到‘编辑页’,编辑完后再返回到‘详情页’,如果‘详情页’数据展示未进行同步修改那肯定是不能接受的。 在webview和5+的混合app模式中,也会遇到这种返回上一个页面不刷新的问题

产生原因

浏览器前进/后退缓存

这里提到一个概念,浏览器前进/后退缓存(Backward/Forward Cache, BF Cache),当然也有人叫 disk Cache。 BF Cache 是一种浏览器优化, HTML 标准并未指定其如何进行缓存,因此缓存行为是各浏览器各自实现,所以不尽相同。 由于不是 HTTP 缓存,所以通过头文件缓存设置 no-cache 是无效的。当然也不能以 HTTP 缓存机制来理解 BF Cache。

解决思路

设置浏览历史当前记录

//监听后退返回事件 --解决微信返回不刷新问题
pushHistory: function(){
    window.addEventListener("popstate", function(e) {
        self.location.reload();
    }, false);
    var state = {
        title : "",
        url : "#"
    };
    window.history.replaceState(state, "", "#");
},

/**
* 页面初始化调用pushHistory,监听popstate事件和执行replaceState()
* 当执行replaceState()时,不会触发popstate事件,所以不会重复刷新
* 当在ios微信内置浏览器中执行浏览器前进后退操作时,触发popstate事件,执行location.reload()
* 但是在谷歌浏览器中执行浏览器前进后退操作时,不会触发popstate事件!因为不在一个document中了
* 但是如果手动改变URL的哈希值,比如www.baidu.com# 改成 www.baidu.com#1 会触发popstate事件,执行location.reload()
* 以上如果有一项不理解或不清晰,请往下看原理深究,你会找到答案
**/

原理深究

前端路由实现(history)原理

以前浏览器操作浏览器历史记录主要依据history对象。在它的 proto 继承有 back、forward、go 等函数。 而 HTML5 后新增 popState 来控制浏览历史记录的 api。有可以存储当前历史记录点的 pushState、替换当前历史记录点的 replaceState、和监听历史记录点的 onPopState。

window.history.pushState(state, title, url)

  • state (状态对象): 一个js对象,可以用来存储一些简单的数据。值得一提的是如果被激活的历史记录条目是通过 pushState 或 replaceState 调用而生成的,popState事件的state属性包含历史条目的状态对象的副本。可以从 history.state中读取存储的 state。
  • title (标题): 目前被浏览器忽略了,所以通常传入一个空字符串
  • URL (地址): 新的历史记录条目的地址。值得一提的是调用 pushState() 后浏览器并不会立即加载这个 URL,但可能会在用户重新打开浏览器时等某些情况加载这个 URL。并且新 URL 必须与当前 URL 同源,否则会抛出一个异常。 新 URL 可为绝对路径或者是相对路径。

window.history.replaceState(state, title, url)

  • 相同之处是两个API都会操作浏览器的历史记录,而不会引起页面的刷新,也不会去验证这个新条目对应的网页是否存在。
  • 不同之处在于 pushState 会增加一条新的历史记录,而 replaceState 则会替换当前的历史记录。
  • 两个 API 都绝对不会触发 hashchange 事件,即使新的 URL 与旧的 URL 仅哈希不同也是如此
  • 如果传递了 stateObj,就会更新当前条目关联的状态对象;如果传递了 url,就会替换当前条目的页面地址和更改浏览器地址栏的地址。

window.onpopstate事件

  • 仅在浏览器前进后退操作、history.go/back/forward 调用、hashChange 的时候触发
  • history.pushState 和 history.replaceState 都不会触发这个事件
  • firfox,chrome 在页面首次打开时都不会触发 popstate 事件,但是 safari 会
  • popstate 事件作用范围仅在于一个 document 里面,由于 pushState 和 hashchange 都不会改变网页的内容也就是 document,所以这样的网页里面才能有效使用 popstate。假如我们输入一个网页,并且在它里面添加了 popstate 回调;然后通过链接跳转的方式转到另外一个网页;再点击后退按钮回到第一个网页。这样的情况,第一个网页里面的 popstate 回调,除了有可能因为页面初始化被触发外,浏览器的后退前进是不会触发它的,因为这种方式改变了窗口的 document。但是!!!在 iOS 微信内置浏览器上,重复上述的操作,会触发 onpopstate 事件!,所以我们才能解决在微信浏览器上页面返回不刷新的问题

pushState 和replaceState 的第一个参数 stateObj,会与第三个参数对应的历史条目绑定在一块,当 popstate 事件触发的时候,意味着有新的历史记录条目被激活,在 popstate 的事件对象里面,有一个 state 属性,会返回这个激活条目关联的 stateObj 对象的拷贝。一个历史记录条目只有当它是被 pushState 创建的,或者用 replaceState 改过的,才可能有关联的 stateObj 对象,所以当某些非这2种条件的历史记录条目被激活的时候,可能拿到的 stateObj 就是 null。

禁止返回上一页的一种方案

/**
* 向历史记录中手动添加一条记录
* 用户选择返回的时候,每次都会消耗一个 history 实体,此时触发 popstate 监听事件,再手动添加一条history实体记录
* 所以用户无论点击多少次都会永远留在这个页面了,当然页面也不会刷新
**/
function pushHistory() {
    window.history.pushState(null, null, "#");
    window.addEventListener("popstate", function (e) {
        console.log(e);
        window.history.pushState(null, null, "#");
    }, false);
}

URL中的#

URL 中的 # 就表示的是 URL 的哈希值

  • #代表网页中的一个位置,其右边的字符,就是该位置的标识符。
设置方法:
step1:设置一个锚点<a href="#print">定位到print位置</a>
step2:在页面需要定位的内容加上id="print"。例如:<div id="print"></div> 或者 <a name="print"></a>
测试:step1设置的锚点,step2中id为print的内容会滚动到页面顶端(可观察滚动条的距离)。同时,页面的url末端中会出现 # print的哈希值。
  • HTTP请求不包含#
    #号是用来指导浏览器动作的,对服务器端完全无用。
    在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。
    访问下面的网址: www.w3cschool.cn/#hello 浏览器实际发出的请求时这样的:

  • 改变#不触发网页重载
    单单改变#后的内容,浏览器只会滚动到相应位置,不会重新加载网页。
    浏览器也不会重新向服务器请求页面

  • 改变#会改变浏览器的访问历史
    每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。

  • window.location.hash读取#值
    window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。

  • onhashchange事件
    这是一个HTML 5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。

    // 它的使用方法有三种:
    window.onhashchange = func;
    <body onhashchange="func();">
    window.addEventListener("hashchange", func, false);
    
  • Google抓取#的机制
    默认情况下,Google的网络蜘蛛忽视URL的#部分。
    但是,Google还规定,如果你希望Ajax生成的内容被浏览引擎读取,那么URL中可以使用"#!",Google会自动将其后面的内容转成查询字符串_escaped_fragment_的值。
    比如,Google发现新版twitter的URL:twitter.com/#!/username
    就会自动抓取另一个URL:twitter.com/?escaped_fragment=/username
    通过这种机制,Google就可以索引动态的Ajax内容。

另一种解决办法:pageShow事件

window.addEventListener('pageshow', () => {
  if (e.persisted || (window.performance && 
    window.performance.navigation.type == 2)) {
    location.reload()
  }
}, false)
  • 为了查看页面是直接从服务器上载入还是从缓存中读取,可以使用 PageTransitionEvent 对象的 persisted 属性来判断。如果页面从浏览器的缓存中读取该属性返回 ture,否则返回 false。
  • window.performance对象,performance.navigation.type是一个无符号短整型

TYPE_NAVIGATE (0):

当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址,type值为0

TYPE_RELOAD (1)

点击刷新页面按钮或者通过Location.reload()方法显示的页面,type值为1

TYPE_BACK_FORWARD (2)

页面通过历史记录和前进后退访问时。type值为2

TYPE_RESERVED (255)

任何其他方式,type值为255 这真是我们需要的部分