如何确保在浏览器关闭时完成最后请求?

438 阅读3分钟

在现代Web应用程序中,用户在关闭浏览器标签页或窗口之前发送一些最终请求是一种常见需求。无论是保存用户的最后活动状态,记录用户的日志,还是更新埋点业务数据,确保在用户退出时能成功发送请求是很重要的。

本文将介绍几种实现这一需求的方式及其优缺点。

1. 使用 navigator.sendBeacon

navigator.sendBeacon 是一种发送小数据包到服务器的技术,特别适合在页面卸载时发送请求。它的主要优点是即使在页面卸载过程中也能保证请求被发送。

window.addEventListener('unload', function () {
  const url = '/log-out';
  const data = JSON.stringify({ reason: 'user-closing-browser' });

  navigator.sendBeacon(url, data);
  
  // 二次确认页面关闭
  // event.returnValue = '确定要离开页面?';
});

优点如下:

  • 适合在页面卸载过程中发送小型请求。
  • 不会阻塞页面卸载过程。

缺点如下:

  • 只支持发送小量的数据,通常建议数据量在64KB以内。
  • 请求的处理和响应不能被直接控制或检查。
  • navigator.sendBeacon的兼容性不是很好。
  • unload 事件现代浏览器对其支持不佳,不推荐使用(可替换为beforeunload)。

2. 使用beforeunload + XMLHttpRequest

在 Web 开发中,如果你希望在用户关闭浏览器窗口或标签页前发送最后一个请求,你可以使用 beforeunload 事件来实现。这个事件在用户试图离开页面(例如关闭窗口、刷新页面或导航到另一个页面)时触发。

window.addEventListener('beforeunload', function (event) {
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/log-out', false); // 设置为同步
  xhr.setRequestHeader('Content-Type', 'application/json');
  xhr.send(JSON.stringify({ message: 'User is leaving the page' }));
});

或者使用fetch + keepalive

window.addEventListener('beforeunload', function (event) {
  fetch('/log-out', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ message: 'User is leaving the page' }),
    keepalive: true
  });
});

fetch API 提供了一个 keepalive 选项,可以在页面卸载过程中继续处理请求。与 sendBeacon 类似,keepalive 适用于发送小型请求。

这种方式的优点在于请求是同步的,确保请求在页面关闭前完成。但同时也有一定的缺陷,它会阻塞页面关闭,可能会引起浏览器警告或影响用户体验。

3. 使用 serviceWorkerSync

serviceWorker 的 Sync API 允许在后台进行一些异步任务,即使用户关闭了页面,这些任务仍可以继续进行。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
    return registration.sync.register('send-final-request');
  }).catch(function(error) {
    console.error('Failed to register sync', error);
  });
}

service-worker.js 中,你可以处理 sync 事件:

self.addEventListener('sync', function(event) {
  if (event.tag === 'send-final-request') {
    event.waitUntil(
      fetch('/log-out', {
        method: 'POST',
        body: JSON.stringify({ reason: 'user-closing-browser' }),
        headers: { 'Content-Type': 'application/json' }
      }).catch(function(error) {
        console.error('Sync failed', error);
      })
    );
  }
});

这种方式可以在页面关闭后继续处理后台任务,更适合处理需要长时间运行的任务。但同时它实现较复杂,需要服务工作者的支持,且需要用户允许后台同步。

小结

实现用户关闭浏览器前发送最后一个请求可以通过多种方式完成,包括使用 beforeunload 事件、navigator.sendBeacon API、以及 serviceWorker 的 Sync API。每种方法有其优缺点,具体选择取决于应用的需求、数据量、用户体验以及对可靠性的要求。在实际应用中,常常需要综合考虑这些因素,以选择最适合的实现方式。