12k
All articles

使用 Beacon API 发送后台数据

通过 Beacon API 与 navigator.sendBeacon() 可靠地发送后台数据,结合批处理策略实现不阻塞页面导航的数据分析追踪。

OpenReplay Team
OpenReplay Team
使用 Beacon API 发送后台数据

页面导航不应该被分析请求所阻塞。当用户点击链接或关闭标签页时,传统的 HTTP 请求可能会阻塞或完全失败,导致数据不完整和用户体验不佳。Beacon API 解决方案通过在后台排队请求来改变这种情况,确保数据能够到达服务器而不影响性能。

本文演示了如何实现 navigator.sendBeacon() 来进行可靠的后台数据传输。您将学习到分析跟踪和错误报告等实际应用,了解为什么它在页面转换期间比 fetch() 表现更好,并看到使用现代 JavaScript 的完整实现。

关键要点

  • Beacon API 在后台排队请求,不会阻塞页面导航
  • 使用 navigator.sendBeacon() 进行分析、错误报告和用户行为跟踪
  • 在页面卸载事件期间,信标比 fetch() 更可靠
  • 保持载荷较小并批量处理多个事件以获得最佳性能
  • 为不支持 Beacon API 的旧浏览器实现降级方案
  • API 返回一个布尔值,表示成功排队,而不是传输确认

Beacon API 的独特之处

Beacon API 方法解决了一个根本问题:传统的 HTTP 请求会阻塞页面导航。当您在卸载事件期间使用 fetch()XMLHttpRequest 时,浏览器必须等待请求完成才能允许导航继续。这会造成糟糕的用户体验和不可靠的数据传输。

Beacon API 的工作方式不同。它异步排队请求并在后台处理传输,即使在页面关闭后也是如此。浏览器管理整个过程,不会阻塞导航或要求您的 JavaScript 保持活跃状态。

为什么传统方法在页面转换期间会失败

考虑使用 fetch() 的这个常见场景:

window.addEventListener('beforeunload', () => {
  // 这可能会失败或阻塞导航
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify({ event: 'page_exit' })
  })
})

这种方法有几个问题:

  • 当页面卸载时请求可能被取消
  • 导航被阻塞直到请求完成
  • 不能保证数据会到达服务器
  • 延迟的页面转换导致糟糕的用户体验

navigator.sendBeacon() 方法接受两个参数:URL 端点和可选数据。它返回一个布尔值,表示请求是否成功排队(不是是否到达服务器)。

const success = navigator.sendBeacon(url, data)

浏览器处理实际的传输,根据网络条件和系统资源进行优化。这种即发即忘的方法非常适合分析、日志记录和诊断数据,在这些场景中您不需要响应。

实际实现示例

基础分析跟踪

这是跟踪用户会话的最小实现:

// 跟踪会话数据
const sessionData = {
  userId: 'user123',
  sessionDuration: Date.now() - sessionStart,
  pageViews: pageViewCount,
  timestamp: new Date().toISOString()
}

// 当页面变为隐藏或用户导航离开时发送数据
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    const payload = JSON.stringify(sessionData)
    navigator.sendBeacon('/log', payload)
  }
})

window.addEventListener('beforeunload', () => {
  const payload = JSON.stringify(sessionData)
  navigator.sendBeacon('/log', payload)
})

错误报告系统

Beacon API 在捕获和报告 JavaScript 错误方面表现出色:

window.addEventListener('error', (event) => {
  const errorData = {
    message: event.message,
    filename: event.filename,
    line: event.lineno,
    column: event.colno,
    stack: event.error?.stack,
    userAgent: navigator.userAgent,
    timestamp: Date.now()
  }

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/log', JSON.stringify(errorData))
  }
})

用户行为跟踪

跟踪特定的用户交互而不阻塞界面:

function trackUserAction(action, details) {
  const actionData = {
    action,
    details,
    timestamp: Date.now(),
    url: window.location.href
  }

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/log', JSON.stringify(actionData))
  }
}

// 使用示例
document.getElementById('cta-button').addEventListener('click', () => {
  trackUserAction('cta_click', { buttonId: 'cta-button' })
})

document.addEventListener('scroll', throttle(() => {
  const scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  )
  trackUserAction('scroll', { percentage: scrollPercent })
}, 1000))

完整的分析实现

这是一个结合多种跟踪场景的综合示例:

class AnalyticsTracker {
  constructor(endpoint = '/log') {
    this.endpoint = endpoint
    this.sessionStart = Date.now()
    this.events = []
    
    this.setupEventListeners()
  }

  setupEventListeners() {
    // 跟踪页面可见性变化
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.sendBatch()
      }
    })

    // 跟踪页面卸载
    window.addEventListener('beforeunload', () => {
      this.sendBatch()
    })

    // 跟踪错误
    window.addEventListener('error', (event) => {
      this.trackEvent('error', {
        message: event.message,
        filename: event.filename,
        line: event.lineno
      })
    })
  }

  trackEvent(type, data) {
    this.events.push({
      type,
      data,
      timestamp: Date.now()
    })

    // 如果事件太多则发送批次
    if (this.events.length >= 10) {
      this.sendBatch()
    }
  }

  sendBatch() {
    if (this.events.length === 0) return

    const payload = {
      sessionId: this.generateSessionId(),
      sessionDuration: Date.now() - this.sessionStart,
      events: this.events,
      url: window.location.href,
      userAgent: navigator.userAgent
    }

    if (navigator.sendBeacon) {
      const success = navigator.sendBeacon(
        this.endpoint, 
        JSON.stringify(payload)
      )
      
      if (success) {
        this.events = [] // 清除已发送的事件
      }
    }
  }

  generateSessionId() {
    return Math.random().toString(36).substring(2, 15)
  }
}

// 初始化跟踪器
const tracker = new AnalyticsTracker('/log')

// 跟踪自定义事件
tracker.trackEvent('page_view', { page: window.location.pathname })

浏览器支持和降级方案

Beacon API 在现代浏览器中具有出色的支持。对于旧浏览器,实现优雅的降级方案:

function sendData(url, data) {
  if (navigator.sendBeacon) {
    return navigator.sendBeacon(url, data)
  }
  
  // 旧浏览器的降级方案
  try {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', url, false) // 卸载事件使用同步
    xhr.setRequestHeader('Content-Type', 'application/json')
    xhr.send(data)
    return true
  } catch (error) {
    console.warn('发送数据失败:', error)
    return false
  }
}

Beacon 实现的最佳实践

保持载荷较小

Beacon API 专为小数据包设计。将载荷限制为必要信息:

// 好的做法:专注的、必要的数据
const essentialData = {
  event: 'conversion',
  value: 29.99,
  timestamp: Date.now()
}

// 避免:大量、不必要的数据
const bloatedData = {
  event: 'conversion',
  value: 29.99,
  timestamp: Date.now(),
  entireDOMState: document.documentElement.outerHTML,
  allCookies: document.cookie,
  completeUserHistory: getUserHistory()
}

批量处理多个事件

不要为每个事件发送单独的信标,而是将它们批量处理:

const eventBatch = []

function addEvent(eventData) {
  eventBatch.push(eventData)
  
  // 当批次达到一定大小时发送
  if (eventBatch.length >= 5) {
    sendBatch()
  }
}

function sendBatch() {
  if (eventBatch.length > 0) {
    navigator.sendBeacon('/log', JSON.stringify(eventBatch))
    eventBatch.length = 0 // 清空批次
  }
}

优雅地处理网络故障

由于信标不提供响应反馈,实现客户端验证:

function validateAndSend(data) {
  // 验证数据结构
  if (!data || typeof data !== 'object') {
    console.warn('信标数据无效')
    return false
  }

  // 检查载荷大小(浏览器通常限制为 64KB)
  const payload = JSON.stringify(data)
  if (payload.length > 65536) {
    console.warn('信标载荷过大')
    return false
  }

  return navigator.sendBeacon('/log', payload)
}

页面转换期间 Beacon API 与 Fetch 的对比

关键差异在页面卸载事件期间变得明显:

// Beacon API:非阻塞、可靠
window.addEventListener('beforeunload', () => {
  navigator.sendBeacon('/log', JSON.stringify(data)) // ✅ 可靠
})

// Fetch API:可能阻塞或失败
window.addEventListener('beforeunload', () => {
  fetch('/log', {
    method: 'POST',
    body: JSON.stringify(data),
    keepalive: true // 有帮助但不保证成功
  }) // ❌ 在导航期间可能失败
})

fetch 请求中的 keepalive 标志提供了类似的功能,但可靠性不如专门为此用例设计的 Beacon API。

结论

Beacon API 为后台数据传输提供了强大的解决方案,不会阻塞页面导航。通过异步排队请求,它确保可靠的数据传输,同时保持最佳的用户体验。无论您是在实现分析跟踪、错误报告还是用户行为记录,navigator.sendBeacon() 都提供了传统 HTTP 方法在页面转换期间无法匹配的可靠性和性能。

信标的即发即忘特性使它们非常适合需要发送数据但不需要响应的场景。结合适当的批处理策略和载荷优化,Beacon API 成为现代 Web 应用程序的重要工具,这些应用程序既优先考虑数据收集又优先考虑用户体验。

常见问题

什么是 Beacon API,为什么我应该使用它而不是 fetch?

Beacon API 是一个 JavaScript Web 标准,用于向服务器发送少量数据而无需等待响应。与 fetch 或 XMLHttpRequest 不同,信标请求由浏览器排队并异步发送,使它们非常适合在页面导航事件期间进行分析和日志记录,而传统请求可能会失败或阻塞用户体验。

我可以使用 Beacon API 发送大量数据吗?

不可以,Beacon API 专为小数据包设计,大多数浏览器通常限制为 64KB。如果您需要发送更大量的数据,请考虑批处理较小的请求,或在非关键时间场景中使用带有 keepalive 标志的 Fetch API 等替代方法。

navigator.sendBeacon 在所有浏览器中都能工作吗?

Beacon API 在所有现代浏览器中都有出色的支持,包括 Chrome、Firefox、Safari 和 Edge。它在 Internet Explorer 中不工作。对于旧浏览器支持,使用 XMLHttpRequest 或 fetch 实现降级方案,尽管这些替代方案在页面卸载事件期间可能无法提供相同的可靠性。

我如何知道我的信标请求是否成功?

navigator.sendBeacon 方法返回一个布尔值,表示请求是否被浏览器成功排队,而不是是否到达服务器。由于信标是即发即忘的,您无法访问服务器响应或确定地知道数据是否被接收。这是为了获得最佳性能而设计的。

我应该何时使用 Beacon API 而不是常规的 fetch 请求?

当您需要在页面转换、卸载事件或页面变为隐藏时发送数据,并且不需要响应时,使用 Beacon API。当您需要处理响应、错误或时间对页面导航不重要时,使用 fetch 进行常规 API 调用。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.