【Safari/IOS 兼容性】 从js visibilitychange Safari下无效说开去
一、Safari下问题说明
在 Safari 浏览器下,无论是桌面端 Safari,还是 iOS Safari,visibilitychange 事件不总是触发的。
对于窗口最小化,Tab 隐藏等行为 visibilitychange 事件是正常的,但是如果是点击页面某个链接发生的当前页导航跳转,则 visibilitychange 事件不会触发。
所以,虽然 visibilitychange 看起来兼容性不错,IE10+支持,但是实际使用的时候还是有一些问题的,上述问题在 caniuse 上也是有对应的描述的。
这就会给我们的业务开发带来困扰,例如,有一个数据上报的需求,希望用户不再访问此页面的时候,进行一次数据上报,则如果使用 visibilitychange 事件进行处理,Safari 浏览器下就会有数据异常的情况发生。
document.addEventListener('visibilitychange', function logData() {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/log', { /* 要发送的数据 */ });
}
});
那有没有什么办法解决这个问题呢?
那就是使用 pagehide 事件。
二、和pageshow/pagehide的区别
1. 功能区别
虽然都是有显示与隐藏的含义,但是 visibilitychange 指的是页面的可见与不可见,pageshow/pagehide 指的是页面的进入与离开。
我们可以通过下面一段测试代码了解两者功能上的区别:
<div id="result"></div>
log = function (content) {
result.innerHTML += content + '<br>';
};
window.addEventListener('pageshow', function () {
log('pageshow: 页面显示');
});
window.addEventListener('pagehide', function () {
log('pagehide: 页面隐藏');
});
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
log('visibilitychange: 页面隐藏');
} else {
log('visibilitychange: 页面显示');
}
});
具体描述为:
- 页面进入,包括刷新会触发 pageshow;
- 选项卡切换,只会触发 visibilitychange 显示与隐藏;
- 前进和后退,所有浏览器都会依次触发 pagehide,visibilitychange 和 pageshow;
- 如果是点击某个链接跳转出去,则Safari浏览器会出现不一样的表现。
大家若有兴趣,可以访问这里感受下事件变化的触发。其实上面的第 4 点大家可以在 Safari 浏览器下测试下,点击页面链接然后再返回,会发现 visibilitychange 事件并未执行。
2. 用法区别
'visibilitychange'
事件通常都是挂载在 document 对象上,虽然现在最新的浏览器也支持挂载在 window 对象上,不过由于 Safari 14 之前的版本不支持,因此,是不推荐使用下面的语法的:
window.addEventListener('visibilitychange', () => {});
而 pageshow 和 pagehide 事件都是通过 window 对象进行注册的。
3. 兼容性区别
pageshow 和 pagehide 事件是 IE11 及其以上浏览器支持的,而 visibilitychange 事件是 IE10 及其以上版本支持的。
具体如下截图示意:
虽然 pageshow 和 pagehide 的兼容性略逊一筹,但是人家稳定啊,以及放眼整个世界,使用 IE10 浏览器的用户微乎其微,因为 IE10 就是个过渡版本。
三、unload 和 beforeunload 事件呢?
除非是要兼容古老的 IE 浏览器,以及在桌面端浏览器环境下阻止用户退出网页(如,您写的内容尚未保存,是否退出,如下代码所示),否则,没有任何理由使用 unload 和 beforeunload 事件,尤其是移动端的页面。
window.addEventListener('beforeunload', function (event) {
if (pageHasUnsavedChanges()) {
event.preventDefault();
return event.returnValue = '您写的内容尚未保存,是否退出?';
}
});
因为用户访问完一个页面,往往是直接切换到其他 APP,然后通过杀进程关掉整个浏览器 APP,unload 事件就不会触发。
以及另外一个比较重要的原因,unload 和 beforeunload 会阻止浏览器把页面存入缓存,影响浏览器前进和后退时候的响应速度。
四、痛快点,终极方案是什么?
回到一开始,只是说了 pagehide 解决 Safari 的问题,可具体该如何解决呢?
很简单,判断是不是 Safari 浏览器,然后额外增加一个 pagehide 事件:
document.addEventListener('visibilitychange', function logData() {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/log', postData );
}
});
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
window.addEventListener('pagehide', function () {
navigator.sendBeacon('/log', postData );
});
}
但是,上面的实现其实是有风险的,因为你并不知道哪一天 Safari 浏览器会改变自己的策略,也就是说不定 Safari 16 或者后面某一个版本 pagehide 也会触发 visibilitychange 行为,则上面的代码又会有重复上报的问题。
所以,比较稳妥,且自己不需要动脑子的方法,就是拾人牙慧,使用他人已经做好的项目进行开发,例如谷歌实验室开源的这个名为 PageLifecycle.js 的项目:github.com/GoogleChromeLabs/page-lifecycle
使用如下:
<script src="./lifecycle.es5.js"></script>
<script>
lifecycle.addEventListener('statechange', function(event) {
console.log('状态变化:' + event.oldState + ' → ' + event.newState);
});
</script>
此时,当我们导航跳转再返回,就会出现如下截图所示的输出效果:
您也可以访问这里亲自感受下输出结果。
Safari 下虽然细节上有差异,但是从 passive → hidden 这个状态和 Chrome 浏览器是一致的,如下截图所示:
所以,我们希望页面离开时候上报数据,可以试试下面的代码,理论上应该是没问题的:
lifecycle.addEventListener('statechange', function(event) {
if (event.oldState == 'passive' && event.newState == 'hidden') {
navigator.sendBeacon('/log', postData);
}
});
上述截图除了 passive 和 hidden 这了两个状态,还出现了 active 和 frozen,这些状态都表示什么意思呢,是浏览器原本就有的,还是 PageLifecycle.js 自定义的呢?
都是浏览器都有的,写入规范标准的状态,都属于页面生命周期的一部分。
关于sendBeacon
https://www.cnblogs.com/sybboy/p/16469617.html
五、了解页面的生命周期
完整的页面生命周期状态包括这些:
- ACTIVE 激活
- PASSIVE 未激活(页面可以看到,但焦点不在此页面,打开开发者工具可以触发此状态)
- HIDDEN 隐藏,最小化、标签页切换都属于隐藏
- FROZEN 冻结
- TERMINATED 结束 (页面被关闭)
- DISCARDED 废弃(页面内容被浏览器清空)
其中,从 HIDDEN 状态到 FROZEN 状态之间的变化是有新的 API 事件名称检测的,分别是 resume 事件和 freeze 事件,使用示意如下:
document.addEventListener('freeze', (event) => {
// 页面被冻结
});
document.addEventListener('resume', (event) => {
// 页面解冻了
});
Web 网页完整的生命周期流程见下面的高清大图(看不清可双指放大,或点击小图查看),原图是英文的,源自 google 官方的这篇文章,自己重新翻译了下,方便大家的学习。
DISCARDED 废弃
其中,废弃状态是后来才有的,原本是没有的,目的是为了释放不必要的内存开销。
如果经常使用 Chrome 浏览器,应该都有遇到过这样的现象,就是一个很久没有访问的标签页再切换过去的时候,页面会重新加载一遍。
之所以会加载,是因为浏览器为了节约内存,把这个长时间不使用的页面给废弃了,所有页面的内存、缓存通通舍弃。
我个人是不太喜欢这样的处理的,因为有些页面,特别是图特别多的大型的文档(如 figma 设计稿),每次切换过去,都要重新 loading 一次,很不爽的。
关于这个,可以所啰嗦两句。
原本 IE 时代,Chrome 还没出现的时候,浏览器的标签页,如果你开了多个,只要 1 个崩掉了,整个浏览器都会崩溃,其他的标签页数据就会丢失。
当然 Chrome 出来的时候,其中宣传的一个优点就是每个标签页面独立,A 页面崩溃不会影响 B 页面,但是,这种不崩溃策略是以牺牲内存为代价的,因此,那个时候,经常有网络图戏谑 Chrome 是个内存怪兽
而现在的这种冻结+废弃的策略,虽然省了内存,但是牺牲了用户体验,正所谓鱼和熊掌不可兼得,所以终极解决方法还是加大内存,16G内存走起。
在 Chrome 68 之后,我们可以使用 document.wasDiscarded
判断页面是不是处于废止状态。
以及,也可以在 Chrome 浏览器地址栏中输入 chrome://discards 查看各个页面的状态。
例如,我现在看了下(省略中间十几个大
可以看到,除了几个新打开不久的页面,其他页面都已经 DISCARDED 掉了,惨!
不说了,我要去找运维申请加内存条了。