如何在页面关闭时发送 API 请求
前言
在一些需求背景下,我们需要在页面销毁(关闭/刷新)时将数据同步给后台,比如 记录视频播放进度、页面浏览时长埋点等。
在 window
全局对象上,提供了 beforeunload
事件,会在浏览器窗口关闭或刷新时触发。
要实现这个需求,普遍的做法是在 window.onbeforeunload
监听事件回调中发起 api
请求。
const onBeforeunload = async () => {
// 发起请求
}
window.addEventListener('beforeunload', onBeforeunload);
注意:在移动设备下,一些浏览器并不支持
beforeunload
事件,最可靠的方式是在visibilitychange
事件中处理。
document.addEventListener('visibilitychange', function logData() {
if (document.visibilityState === 'hidden') {
...
}
});
发起请求的方式有如下几种:
ajax
(XMLHttpRequest)sendBeacon
(Navigator.sendBeacon)fetch
(Fetch keepalive)
下面,我们分析对比以上几种方式的优劣及适用性。
一、ajax
早期前后端进行数据交互多数都采用 XMLHttpRequest
方式创建一个 HTTP
请求,默认采用 异步
方式发起请求:
const ajax = (config) => {
const options = Object.assign({
url: '',
method: 'GET',
headers: {},
success: function () { },
error: function () { },
data: null,
timeout: 0,
async: true, // 是否异步发送请求,默认 true 是异步,同步需设置 false。
}, config);
const method = options.method.toUpperCase();
// 1、创建 xhr 对象
const xhr = new XMLHttpRequest();
xhr.timeout = options.timeout; // 设置请求超时时间
// 2、建立连接
xhr.open(method, options.url, options.async); // 第三参数决定是以 异步/同步 方式发起 HTTP 请求
// 设置请求头
Object.keys(options.headers).forEach(key => {
xhr.setRequestHeader(key, options.headers[key]);
});
// 3. 发送数据
xhr.send(['POST', 'PUT'].indexOf(method) > -1 ? JSON.stringify(options.data) : null);
// 4. 接收数据
xhr.onreadystatechange = function () { // 处理响应
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
options.success(xhr.responseText);
} else {
options.error(xhr.status, xhr.statusText);
}
}
};
// 超时处理
xhr.ontimeout = function () { options.error(0, 'timeout') };
// 错误处理
xhr.onerror = function () { options.error(0, 'error') };
// xhr.abort(); // 取消请求
}
对于 ajax 发起异步请求,若在发送过程中 刷新或关闭 浏览器,请求会被自动终止,如下图:
如果想在控制台查看刷新前页面接口调用情况,可勾选
Preserve log
选项,Network 会保留上个页面的请求记录。
可见,异步方式的 ajax 请求被浏览器自动 cancel
取消,无法将数据正常推送到后台。
一种处理方式是改为 同步 ajax 请求方式,在调用 open
建立连接时,第三参数 async
传递 false
表示以 同步方式 发送请求:
xhr.open(method, options.url, false);
但目前,谷歌浏览器已经不允许在页面关闭期间发起 同步 XHR 请求,建议使用 sendBeacon
或者 fetch keep-alive
。我们接着往下看。
二、sendBeacon
navigator.sendBeacon()
方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。
官方链接:developer.mozilla.org/zh-CN/docs/…
它的语法如下:
navigator.sendBeacon(url);
navigator.sendBeacon(url, data);
url
: 指定将要被发送到的网络地址;data
: 可选,是将要发送的 ArrayBuffer、ArrayBufferView、Blob、DOMString、FormData 或 URLSearchParams 类型的数据。return
: 返回值。当用户代理成功把数据加入传输队列时,sendBeacon()
方法将会返回true
,否则返回false
。
navigator.sendBeacon
使用示例如下:
// 通过 Blob 方式传递 JSON 数据
const blob = new Blob(
[JSON.stringify({ ... })],
{ type: 'application/json; charset=UTF-8' }
);
// 发送请求
navigator.sendBeacon(url, blob);
sendBeacon
发送请求有以下几个特点:
- 通过
HTTP POST
请求方式 异步 发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能; - 支持跨域,但不支持自定义
headers
请求头,这意味着:如果用户信息Access-Token
是作为请求头信息传递,需要后台接口支持url querystring
参数传递解析。 - 考虑其兼容性。
三、fetch keep-alive
当使用 fetch()
请求时,如果把 RequestInit.keeplive
设置为 true
,即便页面被终止请求也会保持连接。
fetch(url, {
method: 'POST',
body: JSON.stringify({ ... }),
headers: {
'Content-Type': 'application/json', // 指定 type
},
keepalive: true,
});
推荐使用 Fetch
API 实现「离开网页时,将数据保存到我们的服务器上」。
但它也有一些限制需要注意:
传输数据大小限制
:无法发送兆字节的数数据,我们可以并行执行多个keepalive
请求,但它们的 body 长度之和不得超过64KB
。无法处理服务器响应
:在网页文档卸载后,尽管设置keepalive 的 fetch 请求
可以成功,但后续的响应处理无法工作。所以在大多数情况下,例如发送统计信息,这不是问题,因为服务器只接收数据,并通常向此类请求发送空的响应。
思考
在框架的生命周期,如 React useEffect
可以实现页面关闭时发送 HTTP 请求记录数据吗?
答案是:不可以。
尽管,我们所理解的 useEffect
中的销毁函数会在页面销毁时触发,但有一个前提条件是:程序保活正常运行,即 ReactDOM.render 创建的 FiberRoot 正常运转
。
试想,浏览器页面进行刷新或关闭,React 所启动的应用会直接中断停止,程序中页面定义的 useEffect
将不会被执行。
参考
Navigator sendBeacon页面关闭也能发送请求方法.
fetch keep