网站首页 > 技术文章 正文
我们经常遇到一个经典场景:用户即将关闭页面或浏览器标签,而我们需要在此刻抓住最后的机会,向服务器发送一些重要信息。
然而,这看似简单的需求,在实践中却充满了挑战。传统的异步请求(如 fetch 或 XMLHttpRequest)在页面卸载事件中极有可能被浏览器中断,导致请求失败。
问题的根源:为什么常规请求会失败?
当用户关闭一个标签页时,浏览器会触发一系列页面卸载(Unload)事件,如 pagehide 和 unload。
在这个过程中,任何在 unload 事件处理器中发起的标准异步 fetch 或 XMLHttpRequest 请求都会面临一个问题:请求刚刚发出,页面就已经被销毁了。
由于页面的 JavaScript 执行环境已不复存在,浏览器没有义务继续完成这个请求,因此会主动取消它。
过去,开发者为了解决这个问题,会使用同步的 XMLHttpRequest。它会强制阻塞主线程,直到请求完成。这种方法虽然“有效”,但对用户体验是毁灭性的——它会导致浏览器 UI 卡死,页面无法响应,直到网络请求结束。
那么,我们该如何在不破坏用户体验的前提下,可靠地发送这“最后一封信”呢?
现代解决方案一:navigator.sendBeacon()
navigator.sendBeacon() 是 W3C 专门为解决此类问题而设计的 API。它的核心使命就是:以异步、非阻塞的方式,可靠地将少量数据发送到服务器。
工作原理
当我们调用 sendBeacon() 时,浏览器会将这个请求添加到一个内部队列中,然后立即返回,不会阻塞页面卸载。浏览器会保证在合适的时机(例如在后台)发送这个请求,即使发起请求的页面已经关闭。
特点
- 可靠性高:由浏览器保证发送,不受页面卸载影响
- 异步非阻塞:不影响用户关闭页面的速度和体验
- 使用简单:API 非常直观
- 数据有限制:只能单向发送 POST 请求,且无法自定义请求头(Headers)
代码示例
假设我们需要在用户离开页面时,发送一条包含页面停留时间的分析日志。
// 推荐使用 'pagehide' 事件,它比 'unload' 更可靠
window.addEventListener('pagehide', (event) => {
// event.persisted 为 true 表示页面进入了往返缓存 (bfcache),并未真正卸载
// 这种情况下我们通常不发送信标
if (event.persisted) {
return;
}
const analyticsData = {
timeOnPage: Math.round(performance.now()),
lastAction: 'close_tab',
};
// 将数据转换为 Blob,这是 sendBeacon 支持的格式之一
const blob = new Blob([JSON.stringify(analyticsData)], {
type: 'application/json; charset=UTF-8',
});
// 使用 sendBeacon 发送数据
// 该方法会返回 true (成功加入队列) 或 false (数据过大或格式错误)
const success = navigator.sendBeacon('/log-analytics', blob);
if (success) {
console.log('分析日志已成功加入发送队列。');
} else {
console.error('无法发送分析日志。');
}
});
现代解决方案二:fetch()与 keepalive: true
fetch API 作为现代网络请求的基石,也提供了一种优雅的解决方案。通过在 fetch 的 init 对象中设置 keepalive: true,我们可以告诉浏览器:“这个请求很重要,请在页面卸载后继续完成它。”
工作原理
fetch({ keepalive: true }) 的工作方式与 sendBeacon 类似。它将一个 fetch 请求标记为“持续活动”,使其生命周期可以超越当前页面。浏览器会像处理 sendBeacon 请求一样,在后台处理它。
特点
- 灵活性高:相比 sendBeacon,它支持更多的 HTTP 方法(如 POST, PUT 等),并允许有限的请求头自定义
- API 统一:如果我们项目中已经大量使用 fetch,使用 keepalive 可以保持代码风格一致
- 同样无法处理响应:和 sendBeacon 一样,由于页面已经关闭,我们无法在前端读取或处理服务器返回的响应
代码示例
假设我们需要在用户关闭页面时,自动保存文本编辑器中的草稿。使用 PUT 请求可能更符合 RESTful 风格。
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
return;
}
const draftContent = document.getElementById('editor').value;
if (!draftContent) return;
const draftData = {
content: draftContent,
timestamp: Date.now(),
};
// 使用 fetch 和 keepalive: true
// 注意:即使请求成功,这里的 .then 和 .catch 也可能不会执行,因为页面正在卸载
try {
fetch('/api/drafts/save', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(draftData),
// 这是关键!
keepalive: true,
});
console.log('保存草稿的请求已提交。');
} catch (e) {
// 这个 catch 块很可能不会捕获到网络错误
console.error('提交保存草稿请求时发生错误:', e);
}
});
如何选择?
如果我们的需求是发送简单的分析或日志数据,navigator.sendBeacon() 是最直接、最符合语义的选择。而如果我们需要更大的灵活性,比如使用 PUT 方法更新资源或需要设置特定的请求头,fetch({ keepalive: true }) 是更好的选择。
这两个现代 API,我们可以在不牺牲用户体验的前提下,确保关键数据的成功送达。
猜你喜欢
- 2025-09-14 前端上传切片优化以及实现_页面切片上传
- 2025-09-14 黑客通过钓鱼攻击劫持npm软件包:含chalk、debug等十余个高频库
- 2025-09-14 记录一次网盘资源不给提取码的经历!另类编程思维,Python破之!
- 2025-09-14 前端必读:如何在 JavaScript 中使用SpreadJS导入和导出 Excel 文件
- 2025-09-14 面试官:说说你对options请求的理解
- 2025-09-14 手把手教你写一个简易的微前端框架
- 2025-09-14 腾讯工程师案例实战:大型前端项目的断点调试共享化和复用化实践
- 2025-09-14 js 通过流的方式进行下载_js 下载流文件
- 2025-07-09 跨域资源的共享(CORS)N种用法(跨域资源共享的缩写是?)
- 2025-07-09 Axios CORS 问题处理(axios has been blocked by cors policy)
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)