返回

Vue页面卸载时API调用:sendBeacon和visibilitychange

vue.js

关闭标签页或窗口时执行API调用(Vue)

在开发Vue应用程序时,需要在用户关闭浏览器标签页或窗口时执行一些操作。常见需求是将用户从等待队列或在线状态中移除。本文探讨如何使用户在离开页面时触发API调用,并讨论可靠性问题,并提供解决方案。

问题:beforeunload 的限制

window.addEventListener('beforeunload', ...)事件可以在用户尝试离开页面时触发,非常适合做一些清理工作。问题在于,此事件的执行时间受限。浏览器不保证能在用户关闭页面前完成请求。若API调用耗时过长,可能会被中断,从而导致数据不一致。

beforeunload的本意是为了向用户展示是否确认离开的对话框,并不是为了在后台发起网络请求。所以我们不应该使用此事件来进行网络请求,因为浏览器对此做了诸多限制,例如在事件中同步发起的请求容易被中断,异步发起的请求可能被延迟执行,执行超时时间很短。

解决方案 1:使用 navigator.sendBeacon

navigator.sendBeacon 方法提供了一种可靠的方式在页面卸载时发送数据。它会发起一个异步的HTTP POST请求,在浏览器空闲时发送。并且会忽略当前页面的关闭动作。它的好处包括:

  • 异步性: sendBeacon 不会阻塞页面的卸载,从而提供更好的用户体验。
  • 可靠性: 浏览器会在后台尽可能尝试完成 sendBeacon 的请求,即使页面已卸载。
  • 性能: 专门为页面卸载事件设计,更加高效。
  • 支持范围广: 现代浏览器都已支持该方法。

操作步骤:

  1. 监听 beforeunload 事件。
  2. 构建要发送的数据。 使用 Blob 对象创建,并设定正确的 MIME 类型,服务器端则可以像处理 Content-Type: application/json 的POST请求那样读取 sendBeacon 方法发来的数据。
  3. 使用 navigator.sendBeacon 发送数据。

代码示例:

handleBeforeUnload() {
  const userId = "123";
  const url = `/waiting/remove/user/${userId}`;
  const data = new Blob([JSON.stringify({ userId })], { type: 'application/json' });
  navigator.sendBeacon(url, data);
}


mounted() {
    window.addEventListener('beforeunload', this.handleBeforeUnload);
},
beforeUnmount() {
  window.removeEventListener('beforeunload', this.handleBeforeUnload);
}

注意: sendBeacon 仅支持 POST 请求,而且没有响应回调函数。它适合发送“fire-and-forget”类型的数据。需要注意请求的内容大小。浏览器限制sendBeacon请求的最大数据量,超出限制的数据会被浏览器直接截断,可能导致API调用失败。通常几kb的payload是没有问题的。

解决方案 2:页面可见性 API (visibilitychange)

除了beforeunload事件外,可以使用页面可见性 API (Page Visibility API) 来处理页面状态改变。这个 API 提供更细粒度的控制。例如,当标签页失去焦点时可以执行一些操作。

原理:

document.visibilityState 属性可以返回当前页面是否可见。 document.addEventListener('visibilitychange', ...) 监听页面可见性变化事件, 当 visibilityState 变为 "hidden" 时,页面即将被隐藏或卸载。此时可以调用 API 。 相比 beforeunload, visibilitychange 不会被离开页面的弹窗打断,且发生时间早于beforeunload,有更多的机会成功请求。

操作步骤:

  1. 监听 visibilitychange 事件。
  2. 检查 document.visibilityState 是否为 hidden
  3. 构建要发送的数据,发送 API 请求。 此方法可以使用传统 fetch 方法,或配合sendBeacon使用。

代码示例:

handleVisibilityChange() {
  if (document.visibilityState === 'hidden') {
    const userId = "123";
      const url = `/waiting/remove/user/${userId}`;
        const data = new Blob([JSON.stringify({ userId })], { type: 'application/json' });
        navigator.sendBeacon(url, data);
      }
}

mounted() {
    document.addEventListener('visibilitychange', this.handleVisibilityChange);
},
beforeUnmount() {
    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}

注意:
虽然 visibilitychange 多数情况下都会在页面离开时触发,但以下情况可能不触发,例如用户直接杀死浏览器进程等。这时,后端需要设置一些额外的定时任务或检查机制以确保能检测到用户离线并将其从列表中移除。 另外需要区分用户的最小化浏览器,离开页面和刷新等情况。 在某些设备如手机设备,用户把程序放入后台会导致页面失去可见性, 需要根据具体需求判断何时发出API调用请求。

解决方案3: 结合 sendBeacon 和服务器端检查

由于客户端的方法无法保证100%的执行,我们应该在服务器端采取一些额外的机制以确保数据的最终一致性。
例如可以为在线用户设置一个定时过期时间。

操作步骤:

  1. 使用 sendBeacon 发送用户的下线信息。 当用户使用上述方法离开页面时,发送信息。
  2. 在服务器端添加定期任务或检查机制: 维护在线用户列表并为用户记录最后一次活跃时间, 例如心跳请求,如果没有收到心跳或离开页面的信号,则定时清理数据。

额外建议

  • 节流请求: 为避免服务器压力, 可以使用 lodash.debounce 或类似的方法进行 API 请求节流。例如在beforeunload 事件多次触发(例如重复按键导致关闭多个窗口),可以使用节流来减少触发频率。
  • 服务器端健壮性: 始终假设API调用可能失败。 在服务端,应该编写有足够错误处理机制的代码,来防止因为偶然的API请求失败导致用户无法被删除。
  • 数据传递: sendBeacon 方法不支持自定义Header, 但可以修改 Content-Type 为 json 从而支持在body中传递json格式数据,并设置请求类型为application/json,方便在服务器端使用框架处理数据。

综合上述讨论,使用navigator.sendBeacon结合服务器端的数据检查和过期机制, 是处理用户离开时API调用的推荐方案。 可以保证相对的可靠性,以及用户信息的最终一致性。