10

从js visibilitychange Safari下无效说开去

 3 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.
neoserver,ios ssh client

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 有话要说?点击这里


Recommend

  • 29
    • 微信 mp.weixin.qq.com 4 years ago
    • Cache

    从编辑距离聊开去

    有这样一个问题:我们有两个字符串,前一个是 bears ,后一个是  barss ,问,在只允许 插入 字符、 删除 字符、 改变 字符三种操作方式的前提下,从后一个(字符)串变...

  • 5
    • kuricat.com 3 years ago
    • Cache

    从 程序员 的版权说开去

    给程序员的常用 开源许可 与 CC 许可 简明手册 从程序的版权说开去 有使用 GitHub 这一类代码仓库的同学想必都有接触过 License 这个名词, 在我们创建 开源项目 的时候, GitHub 会建议为我们的项目添加 License, 如果你和 kurisu 一样不太清楚...

  • 7
    • iphyer.github.io 3 years ago
    • Cache

    从Linux培训的一些话题引申开去

    9.28号参加在中科院雁栖湖校区举办的开源软件自由日活动,收获良多。特别是最后关于LPIC的演讲。主讲人(抱歉没记住名字)对于Linux的观点更加亲切和友好,不是常见的开源愤青但是对于Linux的热爱溢于言表。主要的演讲Topic是关于LPIC这个Linux水平认证考试的介绍...

  • 7
    • inote.xyz 3 years ago
    • Cache

    从美伊冲突说开去

    从美伊冲突说开去...

  • 3

    浅谈苹果的命名困局:从Pro、Air说开去 西方语境中,13 不是一个好数字,背叛耶稣基督的犹大,据信是最后的晚餐餐桌上第 13 个就座的人。于是,西方人不喜欢数字 13 的传统一直延续到今天,很少有人宴客会邀请 13 个人,12 或 14 人都可以,13 不行。

  • 8
    • 微信 mp.weixin.qq.com 3 years ago
    • Cache

    从滴滴下架顺风车说开去

    从滴滴下架顺风车说开去 Original...

  • 5

    Sorry, your browser doesn't support embedded videos. 当前播放: 从人工智能算法聊开去 |InfoQ《极客有约》00:00 / 00:00 1.25x

  • 9

    创业有风险,我们都知道,但做创业公司的产品经理同样也有着各种难题。本文分析了创业公司产品经理可能会遇到的几种现状,并总结了2点做出改变的经验,希望对你有所启发。

  • 8

    抛开去中心化叙事,我们需要DAO的4个理由  •  16 小时前...

  • 10
    • www.btc798.com 2 years ago
    • Cache

    Web3.0打开去中心应用大门

    Web3.0打开去中心应用大门藏民10小时前2674尝试改变现状是人类的天性,当这一动力与科技、资本、信息、数据结合时,人类社会将不可避免地发生变化,不可避免地朝新兴...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK