基于Beacon API的前端日志极端情况下“最后一眼”收集方案

作者:技术拾光时间:2026-06-29 08:23:00浏览:94

前端“最后一眼”日志:为什么非用 Beacon 不可? 页面即将关闭、刷新或跳转时,常规的 fetch 或 XMLHttpRequest 几乎注定被浏览器直接中断,日志也随之丢失。此时,navigator.sendBeacon 几乎是唯一靠谱的选择——规范白纸黑字写明:即使页面已不可见,它也会尽力将

前端“最后一眼”日志:为什么非用 Beacon 不可?

页面即将关闭、刷新或跳转时,常规的 fetch 或 XMLHttpRequest 几乎注定被浏览器直接中断,日志也随之丢失。此时,navigator.sendBeacon 几乎是唯一靠谱的选择——规范白纸黑字写明:即使页面已不可见,它也会尽力将请求发送出去。并且,它不阻塞卸载流程、不依赖事件循环,在 Chrome 39+、Firefox 31+、Safari 11.1+、Edge 79+ 等主流浏览器中稳定可用。

当然,存在两个硬性限制:仅支持 POST 方法,payload 必须是 ArrayBuffer、Blob、FormData 或 URLSearchParams 类型;直接传递普通字符串或 JSON 对象会静默失败,连错误提示都不会给出。

基于Beacon API的前端日志极端情况下“最后一眼”收集方案

合法且兼容的 Beacon 日志 payload 构造方法

千万不要直接 JSON.stringify(logData) 作为参数传入,那会触发类型错误。必须显式转换为允许的类型:

  • 优先使用 Blob:new Blob([JSON.stringify(logData)], { type: 'application/json' })——兼容性好,服务端收到的 Content-Type 明确,解析无歧义。
  • 若后端只接受表单格式:可使用 new URLSearchParams({ data: JSON.stringify(logData) }),但需注意 URL 编码长度限制(一般 ≤2048 字符)。
  • 避免使用 FormData:在部分 Safari 版本中,卸载阶段 FormData 可能失效,且服务端需额外处理 multipart 解析,增加复杂度。
  • payload 体积控制在 64KB 以内:过大容易被浏览器截断,发送失败概率明显上升。

Beacon 上报前必须完成的三个步骤

Beacon 并非直接发送后就无需关注。遗漏任何一步,日志都可能无法发出或内容残缺:

  • 必须在 beforeunload 或 pagehide 事件中直接调用,不能放在异步回调中(例如 setTimeout(() => sendBeacon(...), 0))——事件上下文一旦退出,Beacon 即失效。
  • 必须检查 navigator.sendBeacon 是否存在:旧版 IE 及部分 WebView 不支持,需要 fallback 到 new Image().src = '/logdata=...' 方案。但需注意 URL 长度与编码安全。
  • 日志数据需提前准备好:不要在 beforeunload 中临时计算性能指标(如 getPerformanceMetrics())或读取 localStorage——这些 I/O 操作很可能来不及完成。

边界问题:页面快速跳转与多 Beacon 竞争

用户连续点击两个链接时,A 页面的 beforeunload 触发,B 页面的 pagehide 触发,但浏览器对同一页面只允许一次 Beacon 发送。如果 A 尚未发送完毕就跳转到 B,B 的 Beacon 会覆盖 A 的请求,导致 A 的“最后一眼”日志丢失。

解决方案并非加锁,而是收敛行为:

  • 仅在 pagehide 中发送 Beacon——它比 beforeunload 更可靠,还能捕获缓存导航。
  • 结合 document.hidden 与 visibilitychange:判断是否真正离开当前域(排除标签页切换等伪卸载场景)。
  • 关键日志(如 JS 错误、支付失败)应在发生时立即使用 Beacon 发送,不要等到卸载——卸载仅作为兜底方案,而非主通道。

真正棘手的是单页应用中的路由跳转:它不触发卸载,因此“最后一眼”日志并不存在。在此场景下,只能依靠主动打点加定期 flush,Beacon 仅负责收集真正的终局日志。