在埋点统计、日志上报、退出保存草稿等场景中,我们经常需要在用户关闭页面或跳转到其他站点时发送一次Ajax请求。但实际情况往往是:请求刚发出去,页面就被浏览器销毁了,服务器根本没收到。jQuery的$.ajax在这种时刻显得格外不可靠,请求会被浏览器直接取消。这篇文章就来深入分析这个问题产生的原因,并给出几套实际可用的解决方案。

一、为什么页面卸载时Ajax请求会被终止
要理解这个问题,首先要明白浏览器对页面生命周期的处理方式。当用户点击关闭按钮、输入新网址或者刷新页面时,浏览器会依次触发beforeunload和unload事件,随后开始销毁当前页面的JavaScript执行环境、DOM树以及所有挂起的网络连接。
关键点在于:Ajax请求是异步的,它依赖页面的JavaScript环境持续存活来等待响应。而页面卸载意味着JavaScript上下文即将销毁,浏览器不会为这个页面继续保留网络连接,所以还在等待中的请求会被直接标记为取消状态。在Chrome开发者工具的Network面板中,你会看到这类请求显示为canceled。
需要注意,请求被取消并不代表服务器一定没收到。如果请求报文已经完整发送到了服务器,只是响应还没回来,那么服务器端的处理可能已经完成。问题主要出现在请求还没发完就被切断的情况,这才是数据丢失的根源。
二、传统方案:同步请求及其弊端
最经典的解法是把异步请求改成同步请求。同步请求会阻塞JavaScript的执行,浏览器必须等请求完成后才能继续卸载页面,这样请求就不会被中途取消了。jQuery中的写法是在$.ajax中设置async: false:
$(window).on('unload', function() {
$.ajax({
url: '/api/log',
method: 'POST',
async: false, // 关键:改为同步请求
data: {
action: 'leave',
page: location.href,
duration: Date.now() - pageStartTime
}
});
});这个方案在桌面浏览器上基本可用,但缺点非常明显。第一,同步请求会阻塞主线程,如果服务器响应慢,用户会感觉页面卡死,关闭浏览器标签页时甚至出现白屏等待。第二,Chrome从版本更新后已经禁止在主线程使用同步XHR,会直接在控制台抛出警告甚至报错。第三,同步请求对GET方式支持较好,但携带复杂数据的场景下不够灵活。
因此同步请求只适合作为了解历史的过渡方案,新项目不应该再依赖这种方式处理卸载上报。
三、推荐方案:fetch的keepalive参数
现代浏览器为fetch API提供了keepalive选项,它专门为页面卸载场景设计。设置了keepalive的请求即使页面被销毁,浏览器也会保证在后台把这个请求发送完毕。它的调用方式是纯Promise风格,不依赖任何库:
window.addEventListener('pagehide', function() {
fetch('/api/log', {
method: 'POST',
keepalive: true, // 页面卸载后浏览器继续完成该请求
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'leave',
page: location.href,
ts: Date.now()
})
});
});keepalive有一个限制需要留意:请求体大小不能超过64KB,超过这个限制请求会失败。对于埋点和日志上报这种小数据量的场景来说完全够用,但不适合用来上传大文件。
如果你的项目还在使用jQuery,可以在卸载场景中单独改用fetch,两者并不冲突。jQuery负责日常业务请求,fetch负责卸载上报,这是一种成本最低的混合方案。
四、最稳妥的方案:navigator.sendBeacon
navigator.sendBeacon是专门为页面卸载数据上报设计的API,它的特点是异步且不需要等待响应,浏览器会把数据交给系统队列,即使页面立刻销毁也能保证发送。它的语法非常简单:
function reportOnLeave(data) {
if (navigator.sendBeacon) {
var blob = new Blob([JSON.stringify(data)], {
type: 'application/json'
});
// sendBeacon返回布尔值,表示请求是否成功加入队列
var queued = navigator.sendBeacon('/api/log', blob);
if (!queued) {
// 队列满时降级为fetch keepalive
fetch('/api/log', {
method: 'POST',
keepalive: true,
body: JSON.stringify(data)
});
}
}
}
window.addEventListener('pagehide', function() {
reportOnLeave({ action: 'leave', page: location.href });
});使用sendBeacon有几个细节要注意。它只能发送POST请求,不能自定义大部分请求头,所以传复杂数据需要借助Blob并指定MIME类型,服务端需要按对应类型解析。它同样受64KB数据量限制,且无法读取响应内容,只适合上报类业务。
另外,事件选择上推荐用pagehide而不是unload。移动端浏览器和一些新版桌面浏览器为了优化性能,已经不再保证触发unload事件,而pagehide的触发可靠性更高,是官方推荐的替代品。
五、移动端适配与完整方案对比
移动端浏览器的页面生命周期更复杂,用户切到后台或直接杀掉进程时,unload类事件可能完全不触发。这时需要配合visibilitychange事件,在页面变为不可见时就提前上报数据:
var reported = false;
function reportOnce() {
if (reported) return;
reported = true;
reportOnLeave({ action: 'leave', page: location.href });
}
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
reportOnce(); // 页面不可见时立即上报,不等到卸载
}
});
window.addEventListener('pagehide', reportOnce); // 桌面端兜底用标志位保证只上报一次,避免用户切后台再切回来时重复发送。这种提前上报的策略比死等卸载事件可靠得多。
综合来看,几套方案的取舍如下表所示:
| 方案 | 是否阻塞 | 兼容性 | 适用场景 |
|---|---|---|---|
| 同步XHR(async:false) | 阻塞 | 逐渐被禁用 | 仅老项目维护 |
| fetch keepalive | 不阻塞 | 现代浏览器 | 卸载时POST上报 |
| sendBeacon | 不阻塞 | 现代浏览器 | 首选上报方案 |
| visibilitychange提前上报 | 不阻塞 | 广泛支持 | 移动端必配 |
实际项目中,推荐的做法是把sendBeacon作为首选,fetch keepalive作为降级,再配合visibilitychange做移动端适配,三者组合基本可以覆盖所有主流浏览器环境,彻底解决页面卸载时数据上报丢失的问题。
jQuery AjaxunloadsendBeacon修改时间:2026-09-15 05:42:31