浏览器页面关闭场景下的数据上报
问题背景
在网页应用中,当用户关闭或刷新页面时,通常需要上报用户行为数据(如停留时间、浏览深度等),但这种场景存在以下挑战:
解决方案一:使用标志位防止重复请求 + 异步请求
window.addEventListener("beforeunload", () => {
// 使用异步请求方式发送
BuriedLogic.saveStayTime(3);
});
class BuriedLogic {
static saveStayTime(type) {
// ...
httpPost(url, params, headers);
}
}
// 使用异步请求
function httpPost(url, params = { param: {}, body: {} }, headers = {}) {
// ...
xhr.open("POST", fullUrl, true); // 使用异步请求
// ...
}
优点:
不会阻塞页面卸载,用户体验更好
实现简单,代码逻辑清晰
缺点:
异步请求在页面关闭时通常会被浏览器取消,请求显示为"cancelled"状态
页面刷新会同时触发
beforeunload
和visibilitychange
事件,导致重复发送请求(但是 2 个接口都发送失败了,所以就没考虑)异步请求在浏览器关闭时的行为不稳定,在某些浏览器或特定条件下可能会成功发送,但不能作为可靠设计依据
没有有效的机制确保请求发送完成
异步请求行为解释:
异步 XMLHttpRequest 在页面关闭时的行为取决于多种因素:
请求发起的时机(如果在页面真正关闭前就已经发送,可能会部分成功)
请求的大小(小请求可能在页面关闭前完成)
浏览器的实现差异(不同浏览器处理页面关闭时的未完成请求方式不同)
网络状况(良好的网络可能增加请求成功的几率)
虽然在某些情况下异步请求可能会成功,但这是不可靠且不可预测的,不应作为稳定的解决方案。
特殊情况说明
在某些浏览器和特定条件下,可能会观察到与预期相反的现象:同步请求在页面关闭时没有发送成功,而异步请求却成功发送。这可能受多种因素影响:
浏览器的具体实现和版本差异
请求的大小和处理时长
网络环境和浏览器的资源分配策略
浏览器对同步请求的特殊限制和性能保护机制
无论如何,这种行为不具有一致性和可靠性,不应该作为设计依据。navigator.sendBeacon
API 仍然是页面关闭场景下最可靠的数据发送方式。
解决方案二:使用 navigator.sendBeacon API(强烈推荐)
// 使用requestType标记请求类型
let requestType = null;
document.addEventListener("visibilitychange", () => {
// 如果是刷新引起的visibilitychange,不重复发送请求
if (document.hidden && requestType !== "beforeunload") {
requestType = "visibilitychange";
BuriedLogic.saveStayTime(2);
}
});
window.addEventListener("beforeunload", (event) => {
requestType = "beforeunload";
// 准备要发送的数据
const data = BuriedLogic.prepareStayTimeData(3);
// 使用navigator.sendBeacon发送数据
if (navigator.sendBeacon) {
const url = `${API_URL_BASE}burialPoint`;
// 构建适合sendBeacon的数据
const blob = new Blob(
[
JSON.stringify({
param: {
key: BuriedLogic.instance.urlKey,
type: 3,
imgId: data.imgId,
sourceUrl: data.sourceUrl,
browsingDepth: data.browsingDepth,
residenceTime: data.activeTime * 1000 || "0",
},
}),
],
{ type: "application/json" }
);
// 发送请求,不需要等待响应
navigator.sendBeacon(url, blob);
} else {
// 降级方案:使用同步XHR请求
BuriedLogic.saveStayTime(3);
}
// 重置标记
setTimeout(() => {
requestType = null;
}, 100);
});
优点:
sendBeacon
API 专为页面卸载场景设计,浏览器保证尽最大努力发送数据不会阻塞页面卸载过程,用户体验更好
即使页面已关闭,浏览器也会在后台完成请求
符合现代浏览器设计理念和最佳实践
比同步 XHR 请求有更高的成功率,特别是在页面关闭场景
缺点:
兼容性问题:部分老旧浏览器不支持此 API(需降级处理)
不支持自定义请求头(除 Content-Type 外)
无法获取响应内容
解决方案三:定期保存数据 + 会话恢复
为了更彻底解决数据丢失问题,可以采用定期保存数据的策略:
// 每5秒保存一次当前的数据到localStorage
setInterval(() => {
const data = {
lastActive: Date.now(),
browsingDepth: BuriedLogic.instance.getBrowsingDepth(),
activeTime: BuriedLogic.instance.activeTime,
// 其他需要保存的数据
};
localStorage.setItem("tempBuriedData", JSON.stringify(data));
}, 5000);
// 在页面加载时检查是否有未发送的数据
window.addEventListener("load", () => {
const tempData = localStorage.getItem("tempBuriedData");
if (tempData) {
try {
const data = JSON.parse(tempData);
// 如果数据时间在10分钟内,则认为是有效的未发送数据
if (Date.now() - data.lastActive < 10 * 60 * 1000) {
// 发送上次未完成的数据
BuriedLogic.sendSavedData(data);
}
} catch (e) {
console.error("解析保存的数据失败", e);
}
// 清除临时数据
localStorage.removeItem("tempBuriedData");
}
});
优点:
即使在页面突然关闭的情况下也能恢复大部分数据
可以与其他解决方案结合使用,提高数据完整性
缺点:
增加了实现复杂度
可能导致用户下次访问时发送的数据不够及时或精确
浏览器兼容性
navigator.sendBeacon
支持情况:
Chrome 39+
Firefox 31+
Safari 11.1+
Edge 14+
IE 不支持
最佳实践
首选 navigator.sendBeacon:这是浏览器专门为页面离开场景设计的 API,成功率最高
提供降级方案,在不支持
sendBeacon
的浏览器中使用同步 XHR 请求使用标志位避免
beforeunload
和visibilitychange
事件重复触发请求对于特别重要的数据,考虑定期保存到 localStorage 并在下次访问时恢复上报
接受部分数据丢失的可能性:在用户体验和数据完整性之间需要平衡
服务端应设计为接受幂等请求,避免重复数据问题
如果可能,考虑在用户操作过程中就定期上报数据,而不只依赖页面关闭时机
结论
页面关闭时的数据上报永远存在一定的不确定性,无论使用哪种技术都不能 100%保证数据上报成功。navigator.sendBeacon
API 是目前最佳解决方案,它专为此场景设计,提供了最高的成功率和最好的用户体验。
对于极其重要的数据,应考虑在用户使用过程中就及时上报,而不是等到页面关闭时才上报。