丑话说在前
丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API。
因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome 、360浏览器的急速模式 等一众基于谷歌Chrome浏览器套皮浏览器上实现,那么在下面我会提供一个简单的方法,但是Edge并不支持该方法。Edge是真牛啊,青出于蓝胜于蓝?
先来看看浏览器在刷新/关闭时的顺序
为了帮助理解我区分浏览器关闭和刷新操作的方法,先来看看浏览器在关闭/刷新时的执行顺序吧~
在浏览器关闭或刷新页面时,onbeforeunload
和 onunload
事件的执行顺序是固定的。
- 当用户关闭浏览器标签、窗口或者输入新的 URL 地址时,首先会触发
onbeforeunload
事件。 - 在
onbeforeunload
事件处理完成后,如果用户选择离开页面(关闭或刷新),则会触发onunload
事件。
因此,onbeforeunload
事件在用户决定离开页面之前执行,而 onunload
事件在用户离开页面之后执行。这两个事件提供了在用户离开页面前后执行代码的机会,可以用于执行清理操作或者提示用户确认离开等操作。通过对比两个事件的执行时间差,我们就可以简单判断浏览器的关闭或刷新行为啦。
简易判断Chrome浏览器关闭或刷新行为的方法
ini
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左右。环境不同亦有可能导致结果不同。
详见他人的测试结果图:
如何发送请求
既然已经区分了Chrome浏览器的关闭和刷新行为,那么该如果发送请求呢?
发送请求的方式主要有以下几种:
1. 使用 Navigator.sendBeacon()
该方法主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术,如XMLHttpRequest
所导致的各种问题。
他的使用方法也很简单:
kotlin
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()
的替代品。
php
fetch('url',{
method:'GET',
keepalive:true
})
3. 直接发送异步请求
由于从Chrome83开始,onunload里面不允许执行同步的XHR,所以同步请求自然是无法实现的,但是一部请求是可以实现的。但是异步请求发送到设备的成功率并非百分之百,因此并不推荐,也不在此赘述。
总结
以上便是浏览器关闭/刷新前发送请求的几种方法,而我是采用了 fetch + alive
尝试简单实现浏览器仅关闭时发送请求,具体实现代码如下:
ini
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这也就努力挣扎一下,把测试结果发给评审人员评审一下吧~