使用sendBeacon进行前端数据上报

166 阅读6分钟

使用sendBeacon进行前端数据上报

目录

[TOC]

前言

这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向 Web 服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest

过去,为了解决这个问题,统计和诊断代码通常要在

  • 发起一个同步 XMLHttpRequest 来发送数据。
  • 创建一个 元素并设置 src ,大部分用户代理会延迟卸载(unload)文档以加载图像。
  • 创建一个几秒的 no-op 循环。

上述的所有方法都会迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力。

这就是 sendBeacon() 方法存在的意义。使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能,这意味着:

  • 数据发送是可靠的。
  • 数据异步传输。
  • 不影响下一导航的载入。

数据是通过 HTTP POST 请求发送的。

上报数据的时机
  • 页面加载时

此时进行数据上报,只需要在页面 load 时上报即可。

| ``` window.addEventListener('load', reportData, false);

|:---:|




- 页面卸载或页面刷新时

此时进行数据上报,只需要在页面 `beforeunload` 时上报即可。

> 过去,许多网站使用 [unload](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/unload_event) 或 [beforeunload](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/beforeunload_event) 事件以在会话结束时发送统计数据。然而这是不可靠的,在许多情况下(尤其是移动设备)浏览器不会产生 `unload` 、 `beforeunload` 或 `pagehide` 事件。下面列出了一种不触发上述事件的情况:
> 
> 
> 1. 用户加载了网页并与其交互。
> 2. 完成浏览后,用户切换到了其他应用程序,而不是关闭选项卡。
> 3. 随后,用户通过手机的应用管理器关闭了浏览器应用。
> 
> 此外, `unload` 事件与现代浏览器实现的往返缓存( [bfcache](https://web.dev/articles/bfcache) )不兼容。在部分浏览器(如:Firefox)通过在 bfcache 中排除包含 `unload` 事件处理器的页面来解决不兼容问题,但这存在性能损失。其他浏览器,例如 Safari 和 Android 上的Chrome浏览器则采取用户在同一标签页下导航至其他页面时不触发 `unload` 事件的方法来解决不兼容问题。
> 
> Firefox 也会在 bfcache 中排除包含 `beforeunload` 事件处理器的页面。
> 
>  **使用 pagehide 作为回退** 
> 
> 可使用 [pagehide](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/pagehide_event) 事件来代替部分浏览器未实现的 `visibilitychange` 事件。和 `beforeunload` 与 `unload` 事件类似,这一事件不会被可靠地触发(特别是在移动设备上),但它与 bfcache 兼容。

| ```
window.addEventListener('beforeunload', reportData, false);
``` |
|:---:|





如果是这种情况,可以在 `visibilitychange` 时通过读取 `document.visibilityState` 或 `document.hidden` 区分页面 tab 的激活状态,判断是否需要进行上报。






- SPA 路由切换时
  - 如果是 `vue-router` 或 `react-router@3` 及以下版本,则可以在 hooks 里进行上报操作。
  - 如果是 `react-router@4` 则需要在 `Routes` 根组件的生命周期内进行上报。


- 页面多个 tab 切换时

> 当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 `visibilitychange` 事件。
> 
> 当用户导航到新页面、切换标签页、关闭标签页、最小化或关闭浏览器,或者在移动设备上从浏览器切换到不同的应用程序时,该事件就会触发,其 `visibilityState` 为 `hidden` 。过渡到 `hidden` 是页面能可靠观察到的最后一个事件,因此开发人员应将其视为用户会话的可能结束(例如,用于 [发送分析数据](https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon) )。



| ```
document.addEventListener("visibilitychange", function() {
  if(document.visibilityState === 'visible') {
    reportData();
  }
  if(document.visibilityState === 'hidden') {
    reportData2();
  }
  // your code ...
});
``` |
|:---:|



#### 上报数据的方法

##### 1. 直接发请求上报

我们可以直接将数据通过 ajax 发送到后端,以 `axios` 为例。

| ```
axios.post(url, data);
``` |
|:---:|



但这种方法有一个问题,就是在页面卸载或刷新时进行上报的话,请求可能会在浏览器关闭或重新加载前还未发送至服务端就被浏览器 cancel 掉,导致数据上报失败。

我们可以将 ajax 请求改为同步方法,这样就能保证请求一定能发送到服务端。由于 `fetch` 及 `axios` 都不支持同步请求,所以需要通过 `XMLHttpRequest` 发送同步请求。

| ```
const syncReport = (url, { data = {}, headers = {} } = {}) => {
  const xhr = new XMLHttpRequest();
  xhr.open('POST', url, false);
  xhr.withCredentials = true;
  Object.keys(headers).forEach((key) => {
    xhr.setRequestHeader(key, headers[key]);
  });
  xhr.send(JSON.stringify(data));
};
``` |
|:---:|



这里要注意的是,将请求改为同步以后,会阻塞页面关闭或重新加载的过程,这样就会影响用户体验。

##### 2. 动态图片

我们可以通过在 `beforeunload` 事件处理器中创建一个图片元素并设置它的 `src` 属性的方法来延迟卸载以保证数据的发送,因为绝大多数浏览器会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。

| ```
const reportData = (url, data) => {
  let img = document.createElement('img');
  const params = [];
  Object.keys(data).forEach((key) => {
    params.push(`${key}=${encodeURIComponent(data[key])}`);
  });
  img.onload = () => img = null;
  img.src = `${url}?${params.join('&')}`;
};
``` |
|:---:|



此时服务端可以返回一个 1px * 1px 的图片,保证触发 `img` 的 `onload` 事件,但如果某些浏览器在实现上无法保证图片的载入,就会导致上报数据的丢失。

##### 3. sendBeacon

为了解决上述问题,便有了 [navigator.sendBeacon](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) 方法,使用该方法发送请求,可以保证数据有效送达,且不会阻塞页面的卸载或加载,并且编码比起上述方法更加简单。

用法如下:

| ```
navigator.sendBeacon(url, data);
``` |
|:---:|



url 就是上报地址,data 可以是 `ArrayBufferView` , `Blob` , `DOMString` 或 `Formdata` ,根据官方规范,需要requestheader 为 [CORS-safelisted-request-header](https://fetch.spec.whatwg.org/#cors-safelisted-request-header) ,在这里则需要保证 `Content-Type` 为以下三种之一:


-  `application/x-www-form-urlencoded` 
-  `multipart/form-data` 
-  `text/plain` 

我们一般会用到 `DOMString` , `Blob` 和 `Formdata` 这三种对象作为数据发送到后端,下面以这三种方式为例进行说明。


- DOMString

如果数据类型是 `string` ,则可以直接上报,此时该请求会自动设置请求头的 `Content-Type` 为 `text/plain` 。

| ```
const reportData = (url, data) => {
  navigator.sendBeacon(url, data);
};
``` |
|:---:|




- Blob
- 如果用 `Blob` 发送数据,这时需要我们手动设置 `Blob` 的 MIME type,一般设置为 `application/x-www-form-urlencoded` 。

javascript

| ```
const reportData = (url, data) => {
  const blob = new Blob([JSON.stringify(data)], {
    type: 'application/x-www-form-urlencoded',
  });
  navigator.sendBeacon(url, blob);
};
``` |
|:---:|




- Formdata



可以直接创建一个新的 `Formdata` ,此时该请求会自动设置请求头的 `Content-Type` 为 `multipart/form-data` 。

| ```
const reportData = (url, data) => {
  const formData = new FormData();
  Object.keys(data).forEach((key) => {
    let value = data[key];
    if (typeof value !== 'string') {
      // formData只能append string 或 Blob
      value = JSON.stringify(value);
    }
    formData.append(key, value);
  });
  navigator.sendBeacon(url, formData);
};
``` |
|:---:|



注意这里的 `JSON.stringify` 操作,服务端需要将数据进行 parse 才能得到正确的数据。

#### 总结

我们可以使用 `sendBeacon` 发送数据,这一方法既能保证数据可靠性,也不影响用户体验,如果浏览器不支持该方法,则可以降级使用同步的 ajax 发送数据。