back-forward cache, 为浏览器前进/后退时准备的缓存
官方解释提到了 nsIDOMWindow ,那什么是 nsIDOMWindow 呢?
它是 Gecko 内核标准下的一个 interface, 它主要描述了一个承载了 Document Object Model(DOM)的容器,也就是我们常用的 window 根对象。
与之很像的还有一个叫做 nsIXULWindow 的,它又是干嘛的呢?
XUL的字面意思是 XML-based User Interface Language, stackoverflow 上说它是一个 XUL Application Object Model(AOM) window, 大致看了下 nsIXULWindow 的具体实现, 感觉它应该是浏览器 tab 标签(或新窗口)的实现,那么它和 nsIDOMWindow 的差异就很明显了:
而 nsIDOMWindow 又分为了 outer window 和 inner window, 这两者又代表着什么呢?
outer window 可以理解为浏览器当前上下文,它可以是一个窗口、一个标签甚至是一个 iframe,
inner window 是用户当前所看到的具体内容,下面这张图很好的说明了他们的关系:
蓝色部分是一个个的 inner window, 灰色的框子是 outer window, 在有BFCache的场景里面,你可以认为浏览器只是移动了 outer window 的坐标来直接给你展示已经被 cache 起来的内容
离开将要被 BFCache 的页面时,页面的 dom 状态以及 js 执行状态都将被冻结,再次打开 BFCache 中的页面,dom不会重新渲染,js不会重新执行,window.onload 也不会被触发,计时器会根据离开时的状态继续运行
那么如何判断是不是进入了一个被 BFCache 的页面,或者当前要离开的页面是不是将要被 BFCache? 答案是 pagehide/pageshow
这两个事件对象中都有一个 persisted 属性,当这个属性为 true 时,表示页面将要进入 BFCache 或从 BFCache 中取出(分别对应 pagehide 和 pageshow)
这里有个插曲,在测试 pagehide 的时候,最开始我是用的 alert, 发现一直未执行,后来查证官方文档才发现,页面的离开一共会触发三个事件:
所以我的 alert 才会不被执行,这里可以使用写入 localStorage 的方式来测试
Firefox:
unload
或 beforeunload
cache-control: no-store
cache-control: no-store
Pragma: no-cache
Expires: 0
或者这个值对应的日期比 Date
更小Safari 情况基本和 Firefox 一致
Chrome 基本任何状态的页面都不会进入 BFCache, 为什么呢?答案在这里,现有的 WebKit 内核对 BFCache 的实现和 Chrome 的多进程架构不兼容
更多情况有待补充,Android Browser, Android Chrome, iOS Chrome, Android X5...
有没有特殊情况?
vue@2.5.x 使用 MessageChannel
来修复了一些小问题,但它的存在却导致 safari 的 BFCache 失效,已经在 vue@2.6.x 版本中修复
但这种情况并未在官方文档中有所说明
那我们要怎么知道 BFCache 生效或失效的具体原因呢?大家可以根据这个 tutorial 来配置一个 webkit 项目来调试 PageCache
的 canCacheFrame
方法
上面这种情况 canSuspendActiveDOMObjectsForDocumentSuspension
这个方法返回失败的原因是存在一个叫做 MessagePort
的 activeDOMObject
vue@2.5.22 BFCache 失效
vue@2.6.10 修复 BFCache
使用 vue 或 react 开发的单页面应用我们经常遇到以下场景:
当前页面是一个滚动列表,
向下滚动几屏后点击一个按钮跳转到第二个页面,
然后再点击浏览器自带的返回按钮(或右滑返回)返回当前页面
会发现当前面产生了一块空白且不响应点击的区域,滑动屏幕即恢复正常,审查元素看不到任何内容
注: 仅 iOS(8-12) Safari 有此问题,京东 iOS APP、京东金融 iOS APP 也同样有此问题,但微信 7.0.4 版本首次回退不会有问题,但多次前进后退会复现
其实这跟 vue 或 react 并没有直接的关系,可以试下这个测试页面, 没有使用任何库,在首页下滑几个屏幕后,点击跳一下,然后点击浏览器返回,会发现页面只剩下了 fixed 的按钮部分,灰色的12345内容区域并没有被渲染,滑动屏幕后才会出现
所有js代码如下:
function renderIndex(){
// 这里一定要异步渲染,同步渲染则没有此问题
setTimeout(function(){
document.getElementById('ul').innerHTML = '<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>';
}, 1000);
};
function renderDetail(){
document.getElementById('ul').innerHTML = '<h1 class="detail">Detail</h1>'
};
function render(){
var hash = location.hash;
if(hash === '#detail'){
renderDetail();
}
else{
renderIndex();
}
};
render();
// 监听 hashchange 也会出现同样的问题
window.addEventListener('popstate', function(e){
render();
});
window.addEventListener('pageshow', function(e){
document.getElementById('status').innerHTML = '是否从BFCache中读取: ' + e.persisted;
});
目前还未找到相关的证据,但是按照目前官方文档对 BFCache 的解释:用户点击了链接导致浏览器替换了当前窗口里的内容(new page), 跳转到了新地址,才会发生这一行为,通过上面的代码也可以看到,BFCache 确实没有触发
我们知道,浏览器有记录滚动条位置的行为,具体表现为:打开一个高度大于一个屏幕的页面,向下滚动后刷新浏览器,再次打开的页面仍然是之前滚动条所处的位置
但这是有前提条件的,那就是这个高度必须是 同步渲染的内容所撑开的高度,如果是在js中异步渲染导致撑高的页面,那刷新页面将会丢失滚动条位置并回到顶部
异步渲染丢失滚动条位置,不同的浏览器给出的答案也不一样,Chrome、Firefox 需要一定的渲染延时才会丢失,而 Safari、iOS Safari 只要将渲染放到异步操作中就会丢失
如果跟浏览器重置滚动条的行为有关,那我们在页面初始渲染时就给它一个固定的渲染高度,是不是可以解决这个问题呢?
我们发现浏览器确实都能记录滚动条位置了,那么这种方法能解决上面提到的 SPA 白屏的问题吗?
确实解决了这个问题,但在实际应用中可能有些麻烦,我们需要先计算页面的渲染高度,有没有其他方法呢?
既然出现这个问题后,滑动下页面就恢复正常了,我们或许可以尝试模拟页面滚动行为:跳到下个页面前先记录当前滚动条位置,跳回来后 window.scrollTo
这个位置
经过测试这种方法确实也是可行的,但是需要注意,window.scrollTo
这个操作必须要在页面渲染完成以后
全文完,觉得本文对你有帮助吗?
讨论(4)
我的神呀,,,这都是怎么求证的,,,太厉害了
吊 🐂🍺
解决了
忘了都,在学习学习~