工作踩坑之在浏览器关闭/刷新前发送请求

2,329 阅读5分钟

丑话说在前

丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API

因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome360浏览器的急速模式等一众基于谷歌Chrome浏览器套皮浏览器上实现,那么在下面我会提供一个简单的方法,但是Edge并不支持该方法。Edge是真牛啊,青出于蓝胜于蓝?

先来看看浏览器在刷新/关闭时的顺序

为了帮助理解我区分浏览器关闭和刷新操作的方法,先来看看浏览器在关闭/刷新时的执行顺序吧~

在浏览器关闭或刷新页面时,onbeforeunloadonunload 事件的执行顺序是固定的。

  1. 当用户关闭浏览器标签、窗口或者输入新的 URL 地址时,首先会触发 onbeforeunload 事件。
  2. onbeforeunload 事件处理完成后,如果用户选择离开页面(关闭或刷新),则会触发 onunload 事件。

因此,onbeforeunload 事件在用户决定离开页面之前执行,而 onunload 事件在用户离开页面之后执行。这两个事件提供了在用户离开页面前后执行代码的机会,可以用于执行清理操作或者提示用户确认离开等操作。通过对比两个事件的执行时间差,我们就可以简单判断浏览器的关闭或刷新行为啦。

简易判断Chrome浏览器关闭或刷新行为的方法

let beforeTime = 0,
leaveTime = 0;
// 获取浏览器onbeforeunload时期的时间戳
window.onbeforeunload = () => {
    beforeTime = new Date().getTime();
};
window.onunload = () => {
    // 对比onunload时期和onbeforeunload时期的时间差值
    leaveTime = new Date().getTime() - beforeTime;
    if (leaveTime < 5) {
        // 如果小于5就是关闭
        // 你可以在这发送请求
    } else {
        // 如果大于5就是刷新
        // 你可以在这发送请求
    }
};

注意:经过本人的测试,该方法仅支持Chrome浏览器等,Edge浏览器无论是关闭还是刷新,时间戳差均小于5ms,而谷歌Chrome浏览器的时间戳差均大于5ms,为7ms-8ms左右。环境不同亦有可能导致结果不同。

详见他人的测试结果图:

1703417937476.jpg

如何发送请求

既然已经区分了Chrome浏览器的关闭和刷新行为,那么该如果发送请求呢?

发送请求的方式主要有以下几种:

1. 使用 Navigator.sendBeacon()

该方法主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术,如XMLHttpRequest所导致的各种问题。

他的使用方法也很简单:

navigator.sendBeacon(url, data);

// url参数表明 data 将要被发送到的网络地址
// data (可选) 参数是将要发送的 ArrayBuffer、ArrayBufferView、Blob、DOMString、FormData 或 URLSearchParams 类型的数据。
// 当用户代理成功把数据加入传输队列时,sendBeacon() 方法将会返回 true,否则返回 false。

怎么样?简简单单一行代码即可实现发送可靠的异步请求,同时不会延迟页面的卸载或影响下一导航的载入性能。但是别忽略的他很重要的一个特点数据是通过 POST 请求发送的。

2. 使用 fetch + keepalive

该方法用于发起获取资源的请求。它返回的是一个 promise。他支持 POST 和 GET 方法,配合 keepalive 参数,可以实现浏览器关闭/刷新行为前发送请求。keepalive可用于超过页面的请求。可以说keepalive就是 Navigator.sendBeacon() 的替代品。

fetch('url',{
    method:'GET',
    keepalive:true
})

3. 直接发送异步请求

由于从Chrome83开始,onunload里面不允许执行同步的XHR,所以同步请求自然是无法实现的,但是异步请求是可以实现的。但是异步请求发送到设备的成功率并非百分之百,因此并不推荐,也不在此赘述。

总结

以上便是浏览器关闭/刷新前发送请求的几种方法,而我是采用了 fetch + alive 尝试简单实现浏览器仅关闭时发送请求,以便让后端知道用户关闭了浏览器,从而转为登出状态。具体实现代码如下:

let beforeTime = 0,
leaveTime = 0;
window.onbeforeunload = () => {
    beforeTime = new Date().getTime();
};
window.onunload = () => {
    leaveTime = new Date().getTime() - beforeTime;
    if (leaveTime <= 5) {
        fetch('/logout.do',{
            method:'GET',
            keepalive:true
        })
    }
};

经测试,使用效果如下

使用该方法对于各浏览器的测试结果

浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome登出登出登出未登出
Edge登出未登出登出登出
360急速模式登出登出登出未登出
360兼容模式白屏白屏白屏白屏
IE白屏白屏白屏白屏
浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome
Edge××
360急速模式
360兼容模式××××
IE××××

小小的吐槽:

后端感知web退出本就不推荐由前端来处理,更优解为 持续ping 或者后端 心跳机制发包 来检测。

既然设备那边提出了这个请求,我们web这也就努力挣扎一下,把测试结果发给评审人员评审一下吧~