sendBeacon 返回 true 就代表上报成功了吗?页面离开时怎样发送统计?
区分入队与服务端确认,以页面隐藏时发送统计为例解释 Beacon 的 POST、载荷限制、跨域与隐私边界,以及何时使用 fetch keepalive。
简答
- sendBeacon 返回 true 只表示浏览器接受排队,不代表服务端已收到。
- 适合在页面隐藏时发送一小批统计事件。
- visibilitychange 到 hidden 是可用的发送时机,但不是确定的关闭事件。
- Beacon 只发 POST 且有载荷限制;需要自定义请求头或方法时使用 fetch keepalive。
用户点击链接离开页面,应用想发送最后一批性能统计。普通异步请求可能随文档结束被取消,而同步 XHR 或忙循环会拖慢导航。sendBeacon 为少量分析和诊断数据提供异步发送入口,不需要为了等待上报阻塞用户。
true 只表示浏览器接受排队
sendBeacon 返回布尔值,不返回 Promise 或 Response。true 表示浏览器成功将数据加入发送队列,不表示 DNS 成功、服务器返回 200,更不表示数据已经写入数据库。false 表示未能入队,例如载荷超过可用队列预算。
它没有给脚本读取 HTTP 状态和响应正文的接口。因此支付、订单、保存唯一草稿等必须确认成功的操作,应在用户仍停留页面时使用正常请求协议并展示确认结果。不能在页面关闭时调用 Beacon,然后把本地状态标成“保存成功”。
示例:页面隐藏时发送一小批事件
下面约定服务端同源 /metrics 接收 application/x-www-form-urlencoded,其中 events 字段是 JSON 数组。调用方只传经过白名单筛选的统计字段,不传用户输入、令牌或完整页面 URL。
function queueMetrics(events) {
if (typeof navigator.sendBeacon !== 'function') return false;
const body = new URLSearchParams({ events: JSON.stringify(events) });
// 这是应用自定的保守批次上限,不是浏览器队列剩余容量。
if (new TextEncoder().encode(body.toString()).byteLength > 16 * 1024) {
return false;
}
return navigator.sendBeacon('/metrics', body);
}
let pending = [];
function recordMetric(name, value) {
if (!['menu-open', 'filter-change'].includes(name) || !Number.isFinite(value)) return;
if (pending.length >= 20) return;
pending.push({ name, value });
}
function flushMetrics() {
if (pending.length === 0) return;
const batch = pending;
if (queueMetrics(batch)) pending = [];
}
function onVisibilityChange() {
if (document.visibilityState === 'hidden') flushMetrics();
}
document.addEventListener('visibilitychange', onVisibilityChange);
// 组件或模块销毁时解除监听;不能依赖清理函数保证页面退出上报。
function cleanupMetrics() {
document.removeEventListener('visibilitychange', onVisibilityChange);
}
这里在成功入队后清空内存队列,是明确接受少量丢失的统计策略。入队失败则暂时保留,页面再次可用时可主动重试;刷新或进程终止仍会丢失内存队列。需要更可靠的统计时,应定义事件 ID、持久化、服务端去重和确认协议,而不只是把 false 分支写成无限重试。
示例中的长度按编码后的字节计算,不能用字符串 length 代替 UTF-8 载荷大小。MDN 说明排队数据有 64 KiB 限制,但不能据此保证每个 64 KiB 的请求都能入队:同一环境还可能已有待发送数据。保持批次小,正常浏览期间定期发送,避免把全部数据积攒到离开时。
hidden 是可用机会,不是确定的关闭事件
切换标签页、最小化或切到其他应用都可能使文档隐藏,用户也可能很快回来。因此每次上报只取尚未处理的事件,不能把 hidden 一律解释成会话永久结束。
移动端系统直接终止浏览器进程时,页面不一定收到 unload、beforeunload,甚至不一定有最后一次可运行的回调。优先观察 visibilitychange,并在正常使用中持续提交必要数据,不要试图用离开提示框阻止用户来保证统计完整。
请求方式和跨域限制
Beacon 使用 POST,不能像 fetch 那样任意选择方法或自定义 Authorization 等请求头,也无法读取响应。URLSearchParams 会带相应表单内容类型,Blob 则可以指定 MIME 类型;不要为绕过预检而给 JSON 故意标错 Content-Type。
跨源发送仍受 CORS、Cookie 属性、第三方存储限制和 CSP connect-src 等策略影响。application/json Blob 可能引入预检要求,跨源凭据也不是浏览器必定发送的保证。优先使用同源收集端点,在服务端按协议校验输入、限制大小和频率;统计端点也不能成为任意数据写入或反射入口。
什么时候选择 fetch keepalive
若需要指定方法、请求头或在页面仍活跃时读取响应,可以评估 fetch 的 keepalive 选项。它同样有载荷与生命周期限制,也不让已经销毁的页面继续运行 JavaScript 回调。把 sendBeacon 的 false 分支改成一个大型 keepalive 请求,不是绕过队列限制的可靠方案。
统计通常应做到少量、采样、失败不阻断业务。隐私同意和字段筛选必须在进入队列前完成;改用 Beacon 不会让本来不允许发送的数据变得合规,也不能因为看不到响应就忽略服务器错误监控。