Beacon - 页面关闭请求也能发送成功

4,038 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情


背景

最近在需求中有一个这样的场景:需要在页面关闭的时候,将这个资源释放,那这个资源就可以给其他人使用。

这里最开始有两种方案:

  1. 在页面关闭的时候,向后端发送一个请求,将这个资源释放掉;
  2. 通过 websocket ,当断开连接时后端将资源释放。

经过考虑觉得,单单为了这个场景,使用 websocket有些太重了,所以选择了第一种方式。

定下方案时,觉得也不是什么难事,觉得谷歌浏览器应该会提供页面关闭的 API 供开发者使用。

经过查找,找到了这么两个 API : beforeunloadunload

beforeunload

当浏览器窗口关闭或者刷新时,会触发 beforeunload 事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。

该事件会使网页在离开或者刷新的时候弹出一个对话框,给用户一个提示。在这个弹框出现时,该页面是做不了任何操作的,除非把这个弹框关闭。其他页面也只能进行简单的点击浏览操作,键盘是操作不了的。

使用方式

window.addEventListener('beforeunload', function (event) {
  // Cancel the event as stated by the standard.
  event.preventDefault();
  // Chrome requires returnValue to be set.
  event.returnValue = '';
});

为了用户的安全考虑,浏览器早已不支持向用户自定义消息了。因为诈骗网站经常滥用该功能,会让用户离开页面的时候弹出提示,让用户觉得离开该页面是个错误的操作。

要显示确认对话框的话,需要在事件中调用 event.preventDefault() ,但是不同浏览器有不同的实现方式作为替代:

  • event.returnValue = '' ,即给 returnValue赋值一个字符串;
  • return '' ,在回调函数中 return 一个字符串。

unload

当文档或一个子资源正在被卸载时, 触发 unload 事件。

unload 事件在 beforeunload 事件后触发,这时候文档处于一个什么状态呢?

所有资源都存在,像图片,iframe的等,但是这些资源对于用户来说均不可见,界面上的交互也是无效的.

使用方式和 beforeunload 相同,但是 unload 事件中不能使用确认框,毕竟都已经在卸载了:(

window.addEventListener('unload', function(event) {
  console.log('unload');
});

问题

但是我在实际使用时,却发现了这样几个问题

无法获取用户取消/确认的回调

我拿不到用户在确认框上点击取消/确认的回调,从而导致无论用户取消还是确认,请求都会发送出去,确认对话框失去了意义。

解决方式

那我就退而求其次,在用户关闭时不显示确认框,只要请求能成功发出去就好,需求最重要 :<

无法区分刷新/关闭

我以为浏览器即然能在确认对话框中显示 重新加载此网站离开此网站 的标题,那必然是有提供区分二者的 API,只能说我太天真。我无法区分用户是刷新还是关闭了页面,因为这两种操作都会调用beforeunloadunload

毕竟用户刷新页面是一种很常见的操作,我不能因为用户刷新了页面就将该资源释放。

伪解决方式

有很多帖子都说在页面关闭时才会调用 unload这个API,但其实刷新也会调用,刷新页面文档不也得先卸载嘛(所以说还是自己实验一下为好,不要盲目相信各种帖子)!

╥﹏╥,也有一些比较 hack 的方法,比如通过比较执行 onbeforeunload 与 onunload 之间间隔的时间,但是似乎并不能保证百分百的成功率。

异步请求会被 cancel 掉,导致请求无法发送成功

这是因为异步 AJAX 请求在页面卸载的时候,浏览器有可能发送,也有可能不发送。

或者是在 unload 事件中放一些很耗时的同步操作(比如说给个10000 * 10000的双重 for 循环),留出足够的时间去让异步AJAX发送成功(但是这样做真的不会被用户打死吗)

解决方案1

在事件的回调中使用同步的 AJAX 请求。

window.addEventListener('unload', function (event) {
  let xhr = new XMLHttpRequest();
  xhr.open('post', '/log', false);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.send('foo=bar');
});

但是谷歌浏览器已经不允许页面关闭期间进行同步的 XMLHTTPRequest(),这条规则适用于 beforeunloadunloadpagehidevisibilitychange这些 API,具体内容可以戳这里

解决方案2

为了确保页面在卸载时讲数据发送到服务器,官方建议使用 sendBeacon() 或者 Fetch keep-alive

那我们下面就来看一下这两个方法。

Beacon

这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向web服务器发送数据。

Beacon API 有以下这样几个特点:

  • 通过 HTTP POST 将少量数据异步传输可靠性好
  • 这个请求不需要响应,保证在页面的 unload 状态从发起到完成之前被发送。
  • 不会阻塞页面卸载,也就不会影响下一导航的载入
  • 支持跨域
  • 不支持自定义请求头

语法

const result = navigator.sendBeacon(url, data);

url : 即你要发送的请求的 url

data : 可选参数,即要带给后端的数据,类型可为 ArrayBufferArrayBufferViewBlobDOMStringFormDataURLSearchParams

result : sendBeacon 返回值,如果成功把数据加入传输队列时,返回 true,否则为false。注意,这里的返回值是判定能否成功加入传输队列,而不是请求的返回值。

使用示例

这样就能解决页面卸载时,请求时常被 cancel 掉的问题

window.addEventListener('unload', function (event) {
  const data = {name: "beacon"};
  navigator.sendBeacon('/log', JSON.stringify(data));
});

Fetch keep-alive

keepalive 属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。带有 keepalive 标志的 Fetch 是 Navigator.sendBeacon() API 的替代品。

同样典型的场景就是用户离开网页时,向服务器提交一些用户的行为统计数据。

开启了 keepalive 属性后,网页就算被关闭了,请求被会继续执行而不会中断。

使用示例

window.onunload = function() {
  fetch('/analytics', {
    method: 'POST',
    body: "statistics",
    keepalive: true
  });
};

相比 Beacon API,他有这么一些好处:能自定义请求头不仅仅局限于 POST 请求...

总之只是在 Fetch 添加了一个属性,所以就把他当作一个正常的 Fetch 请求使用就行。

Beacon 和 Fetch keepalive 的限制

当然除了 API 本身,这里也有一些其他的限制:

  • Beacon API 一样,只能传输少量数据,总和通常为 64 KB,这是为了请求能够快速及时的完成,换句话来说,如果并行执行多个 keepalive 请求,数据总和也不能超过 64 KB,如果超过就会返回一个 network error,对传输的数据量有疑问的同学可以戳 这里这里
  • 如果文档已卸载,我们就无法处理服务器响应了,因此在示例中,因为 keepalive,所以 fetch 能成功,但是后续的函数将无法进行正常工作。

但后面发现其实 Beacon 的底层就是通过fetch API来实现的。

总结

因为无法很好的解决如何区分页面刷新还是关闭,毕竟很多用户还是会习惯性的刷新页面,不可能因为刷新就向后端发出请求释放资源,最终我们和产品“友好”的商量了一番,改了一下释放资源的实现方式,完美的解决了这个需求,对这些 API 也有了更深的了解。

以上便是我的浅薄理解,如有不对还忘各位大佬指正。

参考文章