由产品的坚持到对 visibilitychange、pagehide 事件的深入学习

1,852 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1、前言

在公司实习有段时间了,最近开始做需求,其中有个需求是为第三方链接跳转时添加一个提示跳转的中间页,这个需求基本所有做内容的网站都会有;

做完以后测试,测试完产品说我们要实现如果链接是一个下载链接,要关闭中间页,回到之前的页面,不能停留在中间页;

然后我去调研了一下 掘金、csdn、gitee 的实现逻辑,结果都是没有处理这个问题,就是点击继续访问,浏览器下载,中间页还在那里;

image-20220608210849521.png

我就给产品说,业界都是这样啊,如果要区分下载链接处理起来比较麻烦(附带了一个区分下载链接的方案,如下),我想说服产品不要做这个更改~~。


// 方案:通过向第三方链接发送一个请求来判断这个 url 是否是下载链接
request(url).then(res => {
  // 1、判断链接的响应头的 content-type 是否是下载链接
  
  // 2、是就 location.href = url
  
  // 3、不是是就创建一个表单(或 a 标签)来下载,如果是静态文件直接 location.href = url;
})

然后产品说:经过我和设计师的讨论,我们一致认为如果是下载就要关闭中间页;

行~产品说的都对;



2、pagehide

一开始我的思路有两个:

  1. 通过监听下载事件,如果发生了下载就关闭当前的跳转中间页;
  2. 通过提前向 url 发送一个请求判断点击的 url 是否是下载链接,如果是下载链接,点击继续访问下载后,就关闭当前中间页;

然后经过查资料和实验证明,都行不通~:

  1. 并没有监听浏览器下载的事件;
  2. 浏览器的同源政策使得我们根本不能通过 js 发送请求,从而判断响应头字段;即使可以获取 url 的响应体,判断是否是下载链接页是个大问题;

经过我的苦思冥想,换个思路,找到了一个方案(定时器 + pagehide 事件):

  • 点击继续访问时开启一个定时器,1 秒后关闭当前页;

    如果 url 不是下载链接,因为会跳转到新页面,所以会运行 pagehide 事件的监听函数,取消定时器;

    如果 url 是下载链接,因为不会触发 pagehide 事件,就在 1 秒后关闭当前的中间页;

function buttonClickHandler(e) {
    let flag;
    window.addEventListener('pagehide', () => {
        
        clearTimeout(flag);
    })
     
    try{
        const aHref = url;

        ctx.navigate(aHref, {
            openModel: "_self",
            isReplace: true
        })

        flag = window.setTimeout(() => {
            window.close();
        }, 1000)
         
    } catch(e) {
        console.error('页面跳转错误:', e);
    }
    
}

解决这个需求的过程中,查了很多资料,了解了很多以前不知道的内容,特别是 web 页面的事件(load、pageshow 、 beforeunload 、pagehide、unload),和利用 pagehide 和 visitibility 来实现数据上报;

(这里要感慨一下,产品真的是促进开发技术增长的第一生产力)



3、页面级事件

事件触发优先级:load > pageshow > beforeunload > pagehide > unload

  1. load

    当整个页面及所有依赖资源(eg:css 文件、图片等)都已完成加载时,将触发 load 事件;

    如果页面是从浏览器缓存中读取的,不会出发 load 事件;

    和 DOMContentLoaded (初始的 HTML 文档被完全加载和解析完成之后触发,而不需要等待 css文件、图像、iframe 等的加载)有区别;

  2. pageshow

    当 history 对象中的 state 被执行的时候(eg:点击浏览器的前进、后退按钮,使用 history 对象的方法)触发 pageshow 事件;

    当 load 事件加载完后也会触发 pageshow 事件;

    可以通过 e.persisted 判断页面是否是从缓存中获取的;

  3. beforeunload

    触发于 window、document 和它们的资源即将卸载时;

    可以在这个阶段取消默认行为;

    当事件对象的属性 returnValue 被赋值为非空字符串时(或执行 e.preventDefault() 时),会弹出一个对话框,让用户确认是否离开页面;

    在此事件中调用 window.alert()、window.confim()、window.prompt() 时可能会失效;

  4. pagehide

    当浏览器在显示与 history 中当前 state 不同时,会出发 pagehide 事件;

  5. unload

    当 document 或一个子资源正在被卸载时,触发 unload 事件;

  6. visibilitychange

    当选项卡的内容变得可见或被隐藏时,会在 docment 上触发 visibilityState 事件;

    出于兼容性原因(Safari 14 之前的版本不支持挂载在 window 上),要在 document 上监听 visibilityState 事件;

    document.addEventListener("visibilitychange", function () {
      console.log("index visibilityState: ", document.visibilityState); // 返回 hidden 或 visible
    });
    


4、应用(数据上报)

最佳实践:

使用 visibilitychange 事件来处理,并在没有实现 visibilitychange 事件的浏览器中使用 pagehide 来代替;

let hasReport;// 用一个全局标识来防止重复上报
document.addEventListener('visibilitychange', e => {
  if(document.visibilityState === 'hidden') {
    hasReport = true;
    navigator.sendBeacon('/log', somoData); // sendBeacon 通过 post 将少量数据异步传输到 web 服务器;
  }
})
​
// 对 ios 中没有实现 visibilitychange 做兼容性处理;
if(/^((?!chrome|android).)*safari/i.test(navigator.userAgent) && !hasReport) {
  window.addEventListener('pagehide', function () {
        navigator.sendBeacon('/log', someData );
    });
}

注:

  1. 不要使用 unload 和 beforeunload 事件来处理,因为在许多情况下这两个事件并不会被触发;
  2. 可以使用第三方库来完成数据上报:github.com/GoogleChrom…




参考资料:

  1. beforeunload、pagehide、unload 事件的浏览器兼容性测试:cloud.tencent.com/developer/a…
  2. 深度好文: 从js visibilitychange Safari下无效说开去 - 张鑫旭
  3. Navigator.sendBeacon - MDN
  4. sendBeacon 的 polyfill 包:github.com/miguelmota/…
  5. 页面生命周期库:github.com/GoogleChrom…