背景:
有一个任务非常耗时会消耗后台大量算力, 所以在退出页面的时候, 要求前端这边发送一个请求来杀死任务.
一开始以为这个需求非常简单, 就是在进入其他路由前, 发送一下请求, 杀死一下任务就好了.
然而现实狠狠的打了我的脸, 因为退出页面的场景不止切换路由~
退出页面场景:
还在本网站, 跳到其他路由
刷新页面 / 关闭页面也需要发送请求来杀死任务
还在本网站, 跳到其他路由
这个比较简单, 在 vue 中可以通过路由离开的钩子 beforeRouteLeave 来实现:
- beforeRouteLeave(to, from, next) {
- if (任务运行中) {
- // 发送请求
- }else{
- next(true) // 用户离开
- }
- }
刷新页面 / 关闭页面的情况:
然而在刷新页面的时候, beforeRouteLeave 并不会执行, 接着想到了下面这两个 API.
beforeunload 和 unload
beforeunload 当浏览器窗口关闭或者刷新时触发:
介绍:
使用这个 API 可以阻止页面直接关闭, 用户通过点击确定 / 取消按钮, 来决定是否不关闭 / 刷新当前页面.
在 Chrome 下长这个样子, 你们肯定都见过:
如何使用
这个 API 的使用非常简单, 只要在页面加载的时候监听一下此事件, 在需要出现弹窗的时候 return 一个可以转化为 true 的值, 就可以了.
- // 页面卸载之前
- let killTask = false; // 是否杀死任务
- Windows.onbeforeunload = e => {
- if (任务运行 && 对应页面) {
- killTask = true;
- return '您可能有数据没有保存'; // 在部分浏览器可以修改弹窗标题
- } else {
- killTask = false;
- }
- // 没有 return 一个可以转化为 true 的值 就不会出现弹窗
- };
出现此弹窗的浏览器行为:
以下行为是基于 chorme:
焦点: 你没有点击取消 / 确定之前, 焦点会一直在此弹窗上
你无法在出现弹窗的页面上执行任何操作
在其他页面也只能执行简单的点击操作, 弹窗还是存在页面中间, 无法使用键盘,
键盘: 键盘被绑定在弹窗上, 只能通过按键 Esc,Enter 来执行取消 / 确定操作
弹窗不是页面的 dom, 是浏览器的行为
用户取消 / 确定, 没有回调 API, 无法得知
弹窗标题:
Chrome 中刷新页面的标题: 重新加载此网站?
Chrome 中关闭页面的标题: 离开此网站?
现在大部分浏览器都不允许修改弹窗的标题, 这个是为了安全考虑, 来保证用户不受到错误信息的误导,
迷茫:
一开始我以为既然可以拦截到用户的刷新 / 关闭页面的操作, 出现了上面那个弹窗, 这个需求就已经做完了的时候.
然后发现, 浏览器竟然没有提供用户点击确定 / 取消刷新页面的回调.
到这里我陷入了迷茫, 盯着 beforeunload 这个 API 思考了起了人生的意义 (其实是在发呆), 盯着盯着, 从 beforeunload 的 before 我也就想到了 unload 这个 API.
瞬间又燃起了斗志, 何不试试这个 unload?
unload 当页面正在被卸载的时候触发该事件
介绍
当页面正在被卸载的时候触发该事件, 该事件不可取消, 为不可逆操作.
使用
直接监听该事件就可以了.
Windows.onunload = e => {}
结合需求:
killTask 为 beforeunload 时定义的变量, 每次进入回调, 都会给 killTask 赋值, 使用这个值就可以判断什么时候可以发送请求杀死任务.
- Windows.onunload = e => {
- if (killTask && 对应页面) {
- // 发送请求
- }
- };
到这里大家肯定以为已经做出来了该需求, 事实上, 并没有!
无法发送异步请求
我使用的是 axios 来发送请求, 请求发出去了, 但是被取消了, 服务器那边根本没有收到请求, 如下.
经过一顿分析: 发现是 axios 请求是异步的问题, 谷歌之后发现 axios 不支持同步的请求
最后使用原生的 XMLHttpRequest 对象, 让请求同步
大功告成! 实际上, 上面才是我第一次要发的内容, 而下面更好的解决方法!
缺陷与更好的建议:
当我把这篇文章发布在公众号上, 被奇舞周刊 https://mp.weixin.qq.com/s/3taWHBu0vxRXP7WDax5M-Q 转载了, 上面一些大佬给我提了一些建议.
研究了一下, 结果... 好吧! 我承认我是菜鸡.
hey~ 不过这正是我写博客的收获之一, 分享经验, 收获知识!
性能缺陷:
XHR 同步请求会阻碍页面卸载, 如果是刷新 / 跳转页面的话, 页面重新展示速度会变慢, 导致性能问题.
毕竟向网络发送请求并获得响应可能会超级慢, 有可能是用户网络环境比较差, 又或者是服务器挂了, 请求一直没返回回来...
基于性能问题, 大佬们推荐使用 Beacon 代替 XHR, 然后经过一番搜索...
Beacon API
Beacon API 用于将少量数据通过 post 请求发送到服务器.
Beacon 是非阻塞请求, 不需要响应
完美解决性能缺陷问题:
浏览器将 Beacon 请求排队让它在空闲的时候执行并立即返回控制
它在 unload 状态下也可以异步发送, 不阻塞页面刷新 / 跳转等操作.
所以 **Beacon 可以完美解决上面提到的因 XHR 同步请求阻塞而引起的性能缺陷问题 **.
使用:
navigator.sendBeacon()
完整 API:
let result = navigator.sendBeacon(url, data);
Beacon 是挂在 navigator 下面的, 上面就是它的完整 API.
result 是一个布尔值, 代表这次发送请求的结果:
如果浏览器接受并且把请求排队了则返回 tru
如果在这个过程中出现了问题就返回 false
navigator.sendBeacon 接受两个参数:
url: 请求的 URL. 请求是 POST 请求.
data: 要发送的数据. 数据类型可以是: ArrayBufferView, Blob, FormData,Sting.
来看一个用 FormData 来传递数据的栗子, 你就懂了:
- // 创建一个新的 FormData 并添加一个键值对
- let data = new FormData();
- data.append('hello', 'world');
- let result = navigator.sendBeacon('./src', data);
- if (result) {
- console.log('请求成功排队 等待执行');
- } else {
- console.log('失败');
- }
浏览器支持:
浏览器支持: Edge:14+,Firefox:31+,Chrome:39+,Opera:26+,IE: 不支持.
虽然现在浏览器对 sendBeacon 的支持很好, 我们对其做一下兼容性处理也是有必要的:
- if (navigator.sendBeacon) {
- // Beacon 代码
- } else {
- // 回退到 XHR 同步请求或者不做处理
- }
web wroker 中使用 Beacon
因为 Beacon 是挂在 navigator 下面, 而 Web worker 也有 navigator, 去找了一下, 真的给我找到了.
这儿有一个 MDN 提供的栗子, 可以点进去看一下.
PS: 对 Web worker 不熟悉的同学可以看我这篇文章
Beacon 其他相关
客户端优化: 可以将 Beacon 请求合并到其他请求上, 一同处理, 尤其在移动环境下.
Beacon 更多的情况是用于做前端埋点, 监控用户活动, 它的初衷也基于此.
小结
本文总共讲了三个 API,beforeunload,unload 和 Beacon,Beacon 这个 API 估计知道的人比较少, 以后遇到前端埋点和页面卸载前发送请求的需求, 记得使用这三个 API.
以上 2019.02.19
博客 http://obkoro1.com/ , 前端积累文档 http://obkoro1.com/web_accumulate/accumulate/ , 公众号, GitHub https://github.com/OBKoro1
参考资料:
MDN
页面跳转时, 统计数据丢失问题探讨
使用 Web Beacon API 记录活动
来源: https://juejin.im/post/5c3b11e1f265da612415a5f5