🚀 前端必学技巧:用户离开页面时如何可靠地发送 HTTP 请求?
在 Web 应用开发中,我们经常需要在用户离开页面时上报一些数据,例如:
- 记录点击日志
- 上报性能指标
- 发送埋点数据
但是,这里有一个大坑:页面跳转时,HTTP 请求很可能还没发出去就被浏览器取消。如果后端依赖这些日志数据进行分析,那么部分数据就会丢失。
本文将带你从常见方案 → 缺陷分析 → 现代浏览器提供的终极解决方案,完整搞懂这个问题。
❌ 1. 直接使用 fetch
:请求被取消
一种常见写法是给跳转的链接绑定 click
事件,先发送日志请求,再执行跳转:
document.getElementById('link').addEventListener('click', (e) => {
e.preventDefault();
fetch("/log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: 'FedJavaScript' }),
});
window.location = e.target.href;
});
问题来了:
fetch
是异步的,浏览器不会等待它完成- 页面一旦跳转,未完成的请求直接被取消
- 数据可能根本没到服务器
⏳ 2. await fetch
:用户卡住了
那我们是不是可以等待请求完成后,再执行跳转?
document.getElementById('link').addEventListener('click', async (e) => {
e.preventDefault();
await fetch("/log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: 'FedJavaScript' }),
});
window.location = e.target.href;
});
这样确实能保证请求发出去,但有两个问题:
- 移动端 300ms 延迟已经能感知,更别说等一个慢请求了
- 如果
/log
接口返回过慢,用户会觉得页面「卡住了」
显然,这不是好体验。
✅ 3. 现代解决方案:keepalive
好在现代浏览器(Chrome、Safari、Firefox、Edge 等)都支持了 fetch
的 keepalive
参数。
这个参数的作用是:告诉浏览器,即使页面卸载(跳转/关闭),也要尽量把请求完成。
使用方式非常简单:
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: 'FedJavaScript' }),
keepalive: true
});
});
好处是:
- 不需要
preventDefault
,也不需要自己控制跳转 - 不会拖慢页面跳转
- 请求仍然有很高的完成率
📌 4. 其他可选方案
除了 fetch keepalive
,还有一些备选方案:
navigator.sendBeacon
专门为这种「页面卸载时发送少量数据」设计,天然支持后台发送,不会影响跳转。navigator.sendBeacon("/log", JSON.stringify({ name: 'FedJavaScript' }));
Service Worker 缓存再转发
更复杂的方案,把请求写入 Service Worker 缓存,待网络空闲时再补发。
🎯 总结
- 页面跳转时,普通
fetch
请求可能被浏览器取消 await fetch
能保证请求,但会阻塞跳转,影响体验- 推荐方案:使用
fetch
的keepalive: true
或者navigator.sendBeacon
- 在现代前端开发中,这已经是埋点、日志上报的标准实践
所以,下一次你在写日志上报时,记得打开
keepalive
,让数据更可靠、用户体验更流畅 🚀
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>日志上报 Demo</title>
</head>
<body>
<h1>日志上报 Demo</h1>
<p>
点击下面的链接时,会先发送日志,再跳转到百度:
</p>
<a id="link" href="https://www.baidu.com">跳转到百度</a>
<script>
function sendLog(data) {
// 优先使用 sendBeacon
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/log', blob);
} else {
// 兜底用 fetch keepalive
fetch('/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true
});
}
}
document.getElementById('link').addEventListener('click', (e) => {
// 在跳转前先发送日志
sendLog({
event: 'click_link',
href: e.target.href,
time: Date.now()
});
// 不阻止跳转,直接继续
});
</script>
</body>
</html>