2014 年,W3C 釋出了信標(Beacon)的標準草案最終徵求意見稿(目前已經是候選推薦草案)。該規範定義了一個非同步非阻塞的資料上報介面,可以最大限度地減少對其他關鍵操作的資源佔用,同時保證請求能正常發出。同年,該介面就被引入了 Firefox 和 Chrome,即 navigator.sendBeacon(下文簡稱為 sendBeacon)。
在實際開發工作中,該介面最常見的使用場景就是資料埋點。與其他埋點技術方案相比,sendBeacon 的優勢在於:
- 不會跟業務程式碼搶佔資源,而是在瀏覽器空閒的時候再去傳送;
- 在頁面解除安裝(關閉、重新整理、跳轉)時也能保證請求傳送,同時不阻塞頁面解除安裝。
第一個主角——sendBeacon
sendBeacon 的方法原型非常簡單:
navigator.sendBeacon(url, data);
其中:
- data 是將要傳送的資料,可以是 ArrayBuffer、ArrayBufferView、Blob、FormData、URLSearchParams 或字串。
- URL 是 data 將要被髮送到的網路地址。
當資料成功加入到傳輸佇列時,返回值為 true,否則為 false。
一個簡單的呼叫示例如下:
const data = new FormData();
data.append('id', '1');
data.append('type', 'test');
const result = navigator.sendBeacon(url, data);
console.log(result);
考慮到 sendBeacon 可能會存在加入佇列失敗的情況,以及瀏覽器相容性問題,一般來說還需要加上降級支援。
let result;
if (typeof navigator.sendBeacon === 'function') {
result = navigator.sendBeacon(url, data);
}
if (!result) {
const xhr = new XMLHttpRequest();
xhr.open('post', url);
xhr.send(data);
}
除此以外,sendBeacon 還有以下注意要點:
- 該方法的主要使用場景是將少量分析資料傳送給伺服器,以確保請求能夠快速及時地完成,因此它會限制最大的負載體積。
- 該方法總是以 HTTP POST 去傳送請求,且無法設定自定義請求頭或其他與請求、響應相關的屬性。
- 該方法沒有提供獲取響應結果的方式。
- 呼叫該方法時必須以 navigator 作為上下文物件,否則會丟擲 Illegal invocation 的異常。
第二個主角——Blob
如果需要透過 sendBeacon 傳送 JSON 資料,可以這樣呼叫:
navigator.sendBeacon(
url,
new Blob([JSON.stringify(data)], {
type: 'application/json'
})
);
傳送這個請求時,瀏覽器會把 content-type 請求頭設為 application/json。
Chrome 早期版本的安全問題
上述呼叫方式存在一個歷史問題:Chrome 早期支援的 sendBeacon(Chrome 39~58)存在安全風險,跨域請求不會進行預檢,相當於可以跨域提交任何資料。於是,從 Chrome 59 開始,對於跨域請求,瀏覽器不允許設定 content-type 請求頭為 application/x-www-form-urlencoded、multipart/form-data 或者 text/plain 以外的值。一旦出現了這種情況,sendBeacon 就會丟擲異常。
這個問題直到 Chrome 81 才被解決,此後由 sendBeacon 傳送的跨域請求均遵循跨域安全策略,只要 content-type 的值不是上述的三個值之一,就要先進行預檢,預檢透過後才能傳送資料。
考慮到很多瀏覽器或者 app 都是以 Chrome 為核心,並且版本多種多樣,所以如果有傳送 JSON 的需求,可以將內容以 text/plain 的型別傳送,再由後端把文字解析為 JSON 資料。
navigator.sendBeacon(
url,
new Blob([JSON.stringify(data)], {
type: 'text/plain' // 不指定 type 或者指定為空字串也是不行的
})
);
如果必須以 application/json 上報資料,那就需要做降級支援,當 sendBeacon 丟擲異常時降級到 XMLHttpRequest。
let result;
const data = new Blob([JSON.stringify(data)], {
type: 'application/json'
});
try {
result = navigator.sendBeacon(url, data);
} catch (e) { }
if (!result) {
const xhr = new XMLHttpRequest();
xhr.open('post', url);
xhr.send(data);
}
iOS 12 微信下阻塞同域名請求
如果透過 sendBeacon 傳送 application/json 的 Blob,在 iOS 12.7(未測試 iOS 11 和 iOS 12 的其他版本)的微信下還有一個奇怪的問題。
如上方截圖所示,vConsole 顯示 sendBeacon 的請求(api.php)已經發出,但後續幾個同域名請求(get.php)都是 pending 狀態,只有非同域請求(219878)能正常發出。如果用 Charles 或者 Fiddler 抓包,可以發現除 219878 外的所有請求其實都沒有發出。微信社群也有類似問題的反饋。
這種情況在同版本系統的 iOS Safari 下是不會出現的,所以也不知道是什麼原因導致,把 Blob 的 type 改成 text/plain 之後也可以解決。
總結
綜上所述,如果希望透過 sendBeacon 來上報埋點,還是得儘量使用 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 作為 content-type,避免使用 application/json。此外,還必須考慮 sendBeacon 呼叫失敗或入隊失敗時的保底方案。
本文同時發表於作者個人部落格 https://mrluo.life/article/detail/149/send-blob-via-sendbeacon