Navigator.sendBeacon 無阻塞發送統計數據

業務場景

當用戶關閉瀏覽器、刷新瀏覽器或者跳轉其餘頁面時,向服務器發送一些統計數據。前端

常規方案

1. 直接發送 xhr 請求 咱們會優先想到監聽頁面的unload或者beforeunload事件,在事件回調裏使用XMLHttpRequest發送異步請求。npm

可是因爲是xhr請求是異步發送,極可能在它即將發送的時候,頁面已經卸載了,從而致使發送取消或者發送失敗。異步請求響應返回後,因爲頁面和相關資源已經卸載,會引發function not found的錯誤。 後端

解決方法就是 AJAX 通訊改爲同步發送,即只有發送完成,頁面才能卸載。瀏覽器

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));
};
複製代碼

xhr請求改成同步,雖然可以完成發送數據,但存在如下兩個問題:bash

  1. 部分瀏覽器已經不支持同步的 XMLHttpRequest 對象了(即open()方法的第三個參數爲false);
  2. xhr請求改成同步後,會阻塞頁面的卸載和跳轉,致使下一個頁面導航加載的時機變晚,用戶體驗較差。

2. 動態圖片 經過在unload事件處理器中,建立一個圖片元素並設置它的 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('&')}`;
};
複製代碼

這種方法存在一樣的問題,頁面卸載流程被阻塞,後面頁面的加載時機被延遲,用戶體驗很差app

Navigator.sendBeacon

瀏覽器引入的sendBeacon方法,**發出的是異步請求,可是請求是做爲瀏覽器任務執行的,與當前頁面是脫鉤的。**所以該方法不會阻塞頁面卸載流程和延遲後面頁面的加載。 1. 基本用法cors

navigator.sendBeacon(url, data);異步

url 就是上報地址,data 能夠是 ArrayBufferViewBlobDOMStringFormdata,根據官方規範,須要 request header 爲 CORS-safelisted-request-header,在這裏則須要保證 Content-Type 爲如下三種之一:測試

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

咱們通常會用到 DOMString , BlobFormdata 這三種對象做爲數據發送到後端,下面以這三種方式爲例進行說明。

// 1. DOMString類型,該請求會自動設置請求頭的 Content-Type 爲 text/plain
const reportData = (url, data) => {
  navigator.sendBeacon(url, data);
};

// 2. 若是用 Blob 發送數據,這時須要咱們手動設置 Blob 的 MIME type,
// 通常設置爲 application/x-www-form-urlencoded。
const reportData = (url, data) => {
  const blob = new Blob([JSON.stringify(data), {
    type: 'application/x-www-form-urlencoded',
  }]);
  navigator.sendBeacon(url, blob);
};

// 3. 發送的是Formdata類型,
// 此時該請求會自動設置請求頭的 Content-Type 爲 multipart/form-data。
var data = {
   name: '前端名獅子'  ,
   age: 20
};
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);
};
複製代碼

sendBeacon 若是成功進入瀏覽器的發送隊列後,會返回true;若是受到隊列總數、數據大小的限制後,會返回false。返回ture後,只是表示進入了發送隊列,瀏覽器會盡力保證發送成功,可是否成功了,沒法判斷。

2. 發送數據大小限制 目前沒有給出具體的發送數據大小限制標準,不過有人作了下面的測試,當數據長度是65536時,異步請求進入瀏覽器發送隊列失敗,代表數據大小是有限制,不一樣的瀏覽器應該有所差異。:

var url = 'http://jsfiddle.net?sendbeacon';
var n = 65536; // sendBeacon limit for Chrome v40 on Windows (2^16)
var data = new Array(n+1).join('X'); // generate string of length n

if(!navigator.sendBeacon(url, data))
{
   alert('data limit reached');
}
複製代碼

3. 兼容性 sendBeacon方法存在兼容性問題,除了IE,大部分瀏覽器都已經支持,兼容狀況以下圖所示:

考慮到兼容性問題,使用過程當中,咱們能夠採用下面兩種方法作兼容:

  1. 不支持時,用同步的xhr替代,也就是上面提到的第一種方法。
navigator.sendBeacon || new Function('var xhr=new XMLHttpRequest();xhr.open("POST",arguments[0],true);r.send(arguments[1]);');
複製代碼
  1. 補丁文件
// npm 包
npm install navigator.sendbeacon

// 外鏈
<script src="https://unpkg.com/navigator.sendbeacon"></script>
<!-- OR -->
<script src="https://cdn.jsdelivr.net/npm/navigator.sendbeacon"></script>
複製代碼

總結

sendBeacon方法具備以下特色:

  1. 發出的是異步請求,而且是POST請求,後端解析參數時,須要注意處理方式;
  2. 發出的請求,是放到的瀏覽器任務隊列執行的,脫離了當前頁面,因此不會阻塞當前頁面的卸載和後面頁面的加載過程,用戶體驗較好;
  3. 只能判斷出是否放入瀏覽器任務隊列,不能判斷是否發送成功;
  4. Beacon API不提供相應的回調,所以後端返回最好省略response body

關注我

掃一掃 關注個人公衆號【前端名獅】,更多精彩內容陪伴你!

【前端名獅】
相關文章
相關標籤/搜索