在实际项目开发中,可能会遇到这样的业务问题:如何在用户离开或关闭页面时发送HTTP请求给服务端?可能有人会觉得页面都关闭了,还需要发送什么请求,完全没必要噻。但如果真有这样的业务需求落到自己的头上,那么我们应该如何来实现呢?
注:本文章基于Chrome 76,高版本的Chrome浏览器测试效果可能会有差异
关闭或离开页面
可能使用Vue的朋友会比较熟悉beforeDestory
和onBeforeUnMounted
这两个API,用来处理组件销毁前的事件。其实js中也有类似的方法:beforeunload
。
beforeunload
会在浏览器关闭页面或刷新页面时触发,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新,使用方法:
window.addEventListener('beforeunload', (event) => {
// 阻止浏览器默认事件,也就是阻止关闭和刷新页面
event.preventDefault();
// chrome浏览器需要设置返回值
event.returnValue = true;
});
如果是离开页面,应该怎么处理呢?假设我们在页面上有一个链接,点击后会跳转到另一个页面:
<a href="https://baidu.com" id="link">点击跳转</a>
// js
document.getElementById("link").addEventListener('click', (e) => {
e.preventDefault(); // 阻止浏览器默认事件,点击链接就不会发生跳转
window.location = e.target.href;
})
明白了这两个问题后,我们再来看接下来的问题
HTTP请求canceled?
js是单线程的,因此网络请求,包括fetch和XMLHttpRequest请求,被设计成是异步且非阻塞的。异步操作有一个好处,就是它不会占用主进程,但是这也会带来问题,如果主进程销毁了,例如页面关闭或者离开当前页面,那么原来异步进行的网络请求可能会被忽略。直观的体现就是我们可以在network
中看到请求已经canceled
。举个栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<a href="/other.html" id="link">离开页面</a>
<script>
document.getElementById("link").addEventListener("click", (e) => {
fetch("http://localhost:8088/log", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data: "data",
})
});
});
</script>
</body>
</html>
关于这个例子(下同)有几点需要说明:
- 我们使用的是ES6新增的fetch()发送HTTP请求,而不是额外引入axios,关于fetch的使用可以参考阮一峰老师的教程:Fetch API 教程
fetch()
中传入的是请求后端接口地址,我们这里使用的是express
框架搭建的简易后端,本次案例中未实现,详细的使用后面会有单独的文章介绍- 案例模拟的是离开页面的场景,关闭页面需要借助于
beforeunload
和unload
,两者相差不大,掌握原理才是最重要的
执行过程(下同):
- 打开控制台,并选择network,将网络状态选为
slow 3g
,这么做的目的是让我们能更清晰的看到执行过程 - 点击“离开页面”,我们可以看到会有一个
fetch
请求处于pending
状态 - 然后页面跳转到
other.html
,可以发现刚才的log
请求的状态变为了canceled
,也就是被取消了
运行结果:
可以看到,我们刚刚的log请求处于被取消的状态,如果是在实际业务场景中,那么就有可能导致我们的业务请求没有能发送到服务端。那么我们应该怎么解决呢?
如何解决这个问题
解决HTTP请求被canceled的问题,常见的一般有这么几个解决方案:
- async/await
- fetch + keepalive
- navigator.sendBeacon()
- ping
接下来就逐一讨论:
async/await
fetch()
接口返回的是一个Promise()
对象,因此我们可以等待fetch()
接口完成后才执行页面跳转,如果使用的是axios,这里也是一样的:
document.getElementById("link").addEventListener("click", (e) => {
e.preventDefault(); // 阻止默认跳转行为
fetch("http://localhost:8088/log", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
someData: "222",
})
}).then(() => {
window.location = e.target.href; // 页面跳转
});
});
或者使用async/await
document.getElementById("link").addEventListener("click", async (e) => {
e.preventDefault(); // 阻止默认跳转行为
await fetch("http://localhost:8088/log", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
some: "222",
})
});// 同步
window.location = e.target.href; // 页面跳转
});
运行结果:
我们可以明显的看到,虽然点击了链接,但是需要等到请求结束后才会执行跳转,也就是会有一个等待的过程。如果网络请求事件太长,这将会是一个很糟糕的体验。
fetch + keepalive
keepalive
是fetch
的一个属性,目的是告诉浏览器,即使页面卸载了,也要在后台保持连接,继续发送数据。用法也比较简单,直接传入true即可,默认是false:
document.getElementById("link").addEventListener("click", (e) => {
fetch("http://localhost:8088/log", {
method: "POST", // fetch支持GET、POST、PUT、DELETE的请求方法
headers: {
"Content-Type": "application/json",
}, // 请求头
body: JSON.stringify({
some: "222",
}), // 请求数据
keepalive: true, // 保持在后台连接
});
});
使用keepalive
是简单且有效的,那如果我们想要追求更简单的方式呢
navigator.sendBeacon()
navigator.sendBeacon()
方法可用于通过http post的方式将少量数据异步传输到服务器,它的实现原理和传统的XMLHttpRequest有所区别。使用方式一般有两种:
navigator.sendBeacon(url);
navigator.sendBeacon(url, data);
其中,data表示需要发送的Blob、FormData、ArrayBuffer等类型的数据。使用navigator.sendBeacon()
无法自定义请求头部,我们可以借助于Blob对象来简单封装请求头和请求数据:
document.getElementById("link").addEventListener("click", (e) => {
// 自定义请求头
// const blob = new Blob([JSON.stringify({ some: "data" })], { type: 'application/json; charset=UTF-8' });
// navigator.sendBeacon('http://localhost:8088/log', blob);
// 直接发送
navigator.sendBeacon('http://localhost:8088/log');
});
ping
这或许是最简单的解决方式了吧,ping
包含一个以空格分割的url列表,传入的值为字符串,在超链接(a标签)中使用时,浏览器会在后台发送带有正文ping的POST请求。使用方式如下:
<a href="/other.html" id="link" ping="http://localhost:8088/log">离开页面</a>
直接使用在a标签上面即可,注意一定是有意义的a标签,这样写就不行了:
<a id="link" ping="http://localhost:8088/log">离开页面</a>
兼容性:
对比四种方式的优缺点
目前能想到的就这四种,说了这么多,简单总结一下它们的优缺点,其实并没有谁对谁错,只是使用的场景不同。
async/await
- 需要先阻止浏览器的默认事件,等到请求结束后再执行
- 由于需要等待网络请求执行完成,因此会导致用户长时间得不到反馈
- 如果业务场景不在乎等待时间,可以考虑
fetch + keepalive
fetch
接口自带的属性,无需额外引入- 如果请求需要支持GET、POST、PUT、DELETE等,可以选择使用fetch
- 兼容性较好,除了IE不支持
navigator.sendBeacon()
- HTTP请求只能是POST请求
- 发送的数据量少,并且需要更加简洁的API
- 该请求的优先级较低,不会与其他HTTP请求竞争资源
- 兼容性较好,除了IE不支持
ping
- 足够简单,仅依靠HTML就能完成,无需借助JavaScript
- 不会阻塞页面后续行为,与
navigator.sendBeacon()
类似;并且支持跨域 - 目前支持者a标签,其他元素设置ping属性是没有效果的
- 只能是POST请求,不能发送GET请求
- 无法自定义请求数据
- 兼容性很好,除了IE不支持,FireFox默认未启用,需要再FireFox设置中开启
总结
本文总结了前端处理页面关闭时如何保证HTTP请求能顺利发送出去,总的来说有四种方式。每一种方式都有优缺点,需要读者自己权衡如何在实际业务场景种使用。比较推荐的是fetch + keepalive
和navigator.sendBeacon()
。复现文章中的代码,需要先检查一下浏览器版本以及是否清空了缓存。