7

从js visibilitychange Safari下无效说开去

 2 years ago
source link: https://www.zhangxinxu.com/wordpress/2021/11/js-visibilitychange-pagehide-lifecycle/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=10204 鑫空间-鑫生活
本文欢迎分享,欢迎转发,欢迎聚合,全文转载请联系授权。

占位图

关于 visibilitychange 事件,在 2012 年的时候我就撰文介绍过了,详见“Page Visibility(页面可见性) API介绍、微拓展”,是一个非常有历史的 Web 特性,不过实际开发中,由于浏览器实现的细节差异,Page Visibility API 并不总是能够满足实际的生成需求,这是怎么回事呢?

本文就会从 visibilitychange 事件触发,带大家深入了解下 Web 页面的生命周期过程。

一、Safari下问题说明

在 Safari 浏览器下,无论是桌面端 Safari,还是 iOS Safari,visibilitychange 事件不总是触发的。

对于窗口最小化,Tab 隐藏等行为 visibilitychange 事件是正常的,但是如果是点击页面某个链接发生的当前页导航跳转,则 visibilitychange 事件不会触发。

所以,虽然 visibilitychange 看起来兼容性不错,IE10+支持,但是实际使用的时候还是有一些问题的,上述问题在 caniuse 上也是有对应的描述的。

visibilitychange 与 Safari

这就会给我们的业务开发带来困扰,例如,有一个数据上报的需求,希望用户不再访问此页面的时候,进行一次数据上报,则如果使用 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 事件并未执行。

safari visibilitychange未执行示意图

2. 用法区别

'visibilitychange' 事件通常都是挂载在 document 对象上,虽然现在最新的浏览器也支持挂载在 window 对象上,不过由于 Safari 14 之前的版本不支持,因此,是不推荐使用下面的语法的:

window.addEventListener('visibilitychange', () => {});

而 pageshow 和 pagehide 事件都是通过 window 对象进行注册的。

3. 兼容性区别

pageshow 和 pagehide 事件是 IE11 及其以上浏览器支持的,而 visibilitychange 事件是 IE10 及其以上版本支持的。

具体如下截图示意:

pageshow兼容性

虽然 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 会阻止浏览器把页面存入缓存,影响浏览器前进和后退时候的响应速度。

unload 事件兼容性

四、痛快点,终极方案是什么?

回到一开始,只是说了 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>

此时,当我们导航跳转再返回,就会出现如下截图所示的输出效果:

lifecycle.js 示意

您也可以访问这里亲自感受下输出结果。

Safari 下虽然细节上有差异,但是从 passive → hidden 这个状态和 Chrome 浏览器是一致的,如下截图所示:

Safari 下的生命周期状态变化

所以,我们希望页面离开时候上报数据,可以试试下面的代码,理论上应该是没问题的:

lifecycle.addEventListener('statechange', function(event) {
    if (event.oldState == 'passive' && event.newState == 'hidden') {
        navigator.sendBeacon('/log', postData);
    }
});

上述截图除了 passive 和 hidden 这了两个状态,还出现了 active 和 frozen,这些状态都表示什么意思呢,是浏览器原本就有的,还是 PageLifecycle.js 自定义的呢?

都是浏览器都有的,写入规范标准的状态,都属于页面生命周期的一部分。

五、了解页面的生命周期

完整的页面生命周期状态包括这些:

  • 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 是个内存怪兽(例如下面这个 GIF 图 – 1.05M 点击播放)。

Chrome 内存怪兽

而现在的这种冻结+废弃的策略,虽然省了内存,但是牺牲了用户体验,正所谓鱼和熊掌不可兼得,所以终极解决方法还是加大内存,16G内存走起。

在 Chrome 68 之后,我们可以使用 document.wasDiscarded 判断页面是不是处于废止状态。

以及,也可以在 Chrome 浏览器地址栏中输入 chrome://discards 查看各个页面的状态。

例如,我现在看了下(省略中间十几个大同小异):

标签页状态查看

可以看到,除了几个新打开不久的页面,其他页面都已经 DISCARDED 掉了,惨!

不说了,我要去找运维申请加内存条了。

六、例行的结尾内容

好,以上就是本文的全部内容,如果文中有表述不准确的地方,欢迎指正。

风萧萧兮易水寒,写作头发兮不复还,这句诗充分表现了写作的不易与辛苦,连头发都掉没了,所以,求分享,求转发。

1f60e.svg

(本篇完)1f44d.svg 是不是学到了很多?可以分享到微信
1f44a.svg 有话要说?点击这里


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK