前言
最近接到一個需求,需要統計頁面的相關數據,並進行上報,本文就介紹一下數據上報的一些方法。
上報數據的時機
- 頁面加載時
此時進行數據上報,只需要在頁面 load
時上報即可。
window.addEventListener('load', reportData, false);
- 頁面卸載或頁面刷新時
此時進行數據上報,只需要在頁面 beforeunload
時上報即可。
window.addEventListener('beforeunload', reportData, false);
-
SPA 路由切換時
- 如果是
vue-router
或react-router@3
及以下版本,則可以在 hooks 裏進行上報操作。 - 如果是
react-router@4
則需要在Routes
根組件的生命週期內進行上報。
- 如果是
頁面多個 tab 切換時
如果是這種情況,可以在 visibilitychange
時通過讀取 document.visibilityState
或 document.hidden
區分頁面 tab 的激活狀態,判斷是否需要進行上報。
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 方法,使用該方法發送請求,可以保證數據有效送達,且不會阻塞頁面的卸載或加載,並且編碼比起上述方法更加簡單。
用法如下:
navigator.sendBeacon(url, data);
url 就是上報地址,data 可以是 ArrayBufferView
,Blob
,DOMString
或 Formdata
,根據官方規範,需要 request header 爲 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
。
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 發送數據。