【前端技術(shù)】前端實現(xiàn)監(jiān)控 SDK 的全面解析(二)
三、功能拆分
(一)初始化
初始化階段需要獲取用戶傳遞過來的相關(guān)參數(shù),接著調(diào)用初始化函數(shù)。在這個初始化函數(shù)里,能夠注入一些監(jiān)聽事件,以此來達(dá)成數(shù)據(jù)統(tǒng)計的功能。以下是具體的代碼實現(xiàn)示例及解析:
javascript
// 初始化配置
function init(options) {
// ------- 加載配置 ----------
loadConfig(options);
}
// 加載配置
export function loadConfig(options) {
const {
appId, // 系統(tǒng)id
userId, // 用戶id
reportUrl, // 后端url
autoTracker, // 自動埋點
delay, // 延遲和合并上報的功能
hashPage, // 是否hash錄有
errorReport // 是否開啟錯誤監(jiān)控
} = options;
// --------- appId ----------------
if (appId) {
window['_monitor_app_id_'] = appId;
}
// --------- userId ----------------
if (userId) {
window['_monitor_user_id_'] = userId;
}
// --------- 服務(wù)端地址 ----------------
if (reportUrl) {
window['_monitor_report_url_'] = reportUrl;
}
// -------- 合并上報的間隔 ------------
if (delay) {
window['_monitor_delay_'] = delay;
}
// --------- 是否開啟錯誤監(jiān)控 ------------
if (errorReport) {
errorTrackerReport();
}
// --------- 是否開啟無痕埋點 ----------
if (autoTracker) {
autoTrackerReport();
}
// ----------- 路由監(jiān)聽 --------------
if (hashPage) {
hashPageTrackerReport(); // hash路由上報
} else {
historyPageTrackerReport(); // history路由上報
}
}
(二)錯誤監(jiān)控
前端錯誤類型多樣,不同類型的錯誤需要采用不同的捕獲方式來實現(xiàn)全面監(jiān)控。
1. 語法錯誤
這類錯誤通常在開發(fā)階段就能被發(fā)現(xiàn),例如拼寫錯誤、符號使用錯誤等情況。語法錯誤沒辦法通過 try{}catch{} 進(jìn)行捕獲,因為正常在開發(fā)流程中就會被排查出來,基本不會發(fā)布到線上環(huán)境。
2. 同步錯誤
在 JavaScript 同步執(zhí)行過程中產(chǎn)生的錯誤屬于同步錯誤,像變量未定義這類情況,此類錯誤是可以被 try-catch 語句捕獲到的。
3. 異步錯誤
在諸如 setTimeout 等函數(shù)執(zhí)行中出現(xiàn)的錯誤就是異步錯誤,它無法被 try-catch 捕獲,但可以利用 Window.onerror 來進(jìn)行捕獲處理,相較而言這種方式要比 try-catch 方便許多。
4. Promise 錯誤
在 Promise 中,如果使用 catch 語句是可以捕獲到異步錯誤的,然而要是沒寫 catch 的話,在 Window.onerror 里是沒辦法捕獲到這類錯誤的。對此,可以在全局添加 unhandledrejection 監(jiān)聽來捕獲那些沒被捕獲到的 Promise 錯誤。
5. 資源加載錯誤
指的是一些資源文件獲取失敗的情況,一般通過 Window.addEventListener 來進(jìn)行捕獲操作。
綜合來看,SDK 監(jiān)控錯誤主要圍繞以上這些類型來實現(xiàn),try-catch 用于在可預(yù)見的情形下監(jiān)控特定錯誤,Window.onerror 主要負(fù)責(zé)捕獲那些預(yù)料之外的錯誤(比如異步錯誤),但對于 Promise 錯誤和網(wǎng)絡(luò)錯誤無法單純依靠它們捕獲,所以需要借助 Window.unhandledrejection 監(jiān)聽捕獲 Promise 錯誤,通過 error 監(jiān)聽捕獲資源加載錯誤,以此達(dá)成對各類型錯誤的全面覆蓋。
(三)用戶埋點統(tǒng)計
埋點是用于監(jiān)控用戶在應(yīng)用上的各種動作表現(xiàn)的一種手段。
1. 手動埋點
需要手動在代碼中添加相關(guān)的埋點代碼,比如當(dāng)用戶點擊某個按鈕或者提交一個表單時,就在按鈕點擊事件以及提交事件中添加對應(yīng)的埋點代碼。其優(yōu)點在于可控性較強,能夠根據(jù)需求自定義上報具體的數(shù)據(jù)內(nèi)容;但缺點也很明顯,就是對業(yè)務(wù)代碼的侵入性較大,如果需要在很多地方進(jìn)行埋點操作,那就得逐個去添加代碼了。
2. 自動埋點
自動埋點很好地解決了手動埋點的缺點,實現(xiàn)了無需侵入業(yè)務(wù)代碼就能在應(yīng)用里添加埋點監(jiān)控的功能。不過它也存在不足,只能上報基本的行為交互信息,沒辦法上報自定義的數(shù)據(jù),而且只要頁面中有點擊操作,就會向服務(wù)器上報,這可能導(dǎo)致上報次數(shù)過多,給服務(wù)器帶來較大壓力。同時需要注意,如果在 click 事件中阻止了冒泡行為,自動埋點是無法捕獲到的,這種情況下就需要進(jìn)行手動埋點上報,以確保上報的全面覆蓋。
(四)PV 統(tǒng)計
PV 即頁面瀏覽量,表示頁面被訪問的次數(shù)。對于非 SPA 頁面,只需通過監(jiān)聽 onload 事件就能統(tǒng)計頁面的 PV 了。但在 SPA 頁面中,路由的切換主要依靠前端來實現(xiàn),而且單頁面切換又分為 hash 路由和 history 路由,這兩種路由的實現(xiàn)原理不一樣,所以要分別針對它們實現(xiàn)不同的數(shù)據(jù)采集方式。
1. history 路由
history 路由是依賴全局對象 history 來實現(xiàn)的,它包含諸如 history.back()(返回上一頁,對應(yīng)瀏覽器回退操作)、history.forward()(前進(jìn)一頁,即瀏覽器前進(jìn)操作)、history.go()(跳轉(zhuǎn)歷史中某一頁)、history.pushState()(添加新記錄)、history.replaceState()(修改當(dāng)前記錄)等方法。其中 pushState 和 replaceState 這兩個方法不能被 popstate 監(jiān)聽到,因此需要對這兩個方法進(jìn)行重寫,并添加自定義事件監(jiān)聽來實現(xiàn)數(shù)據(jù)采集,以下是具體的代碼實現(xiàn)示例及解析:
javascript
import { lazyReport } from './report';
// history路由監(jiān)聽
export function historyPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁面的時間
let beforePage = ''; // 上一個頁面
// 獲取在某個頁面的停留時間
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime;
beforeTime = curTime;
return stayTime;
}
// 重寫pushState和replaceState方法
const createHistoryEvent = function (name) {
// 拿到原來的處理方法
const origin = window.history[name];
return function(event) {
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
// history.replaceState
window.addEventListener('replaceState', function () {
listener()
});
window.history.pushState = createHistoryEvent('pushState');
window.history.replaceState = createHistoryEvent('replaceState');
function listener() {
const stayTime = getStayTime(); // 停留時間
const currentPage = window.location.href; // 頁面路徑
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// 頁面load監(jiān)聽
window.addEventListener('load', function () {
// beforePage = location.href;
listener()
});
// unload監(jiān)聽
window.addEventListener('unload', function () {
listener()
});
// history.go()、history.back()、history.forward() 監(jiān)聽
window.addEventListener('popstate', function () {
listener()
});
}
2. hash 路由
在 hash 路由中,url 里的 hash 值發(fā)生變化時會觸發(fā) hashChange 的監(jiān)聽,所以只需在全局添加一個監(jiān)聽函數(shù),然后在這個函數(shù)里實現(xiàn)數(shù)據(jù)采集上報就可以了。不過在 React 和 Vue 等框架中,hash 路由的跳轉(zhuǎn)有時候是通過 pushState 實現(xiàn)的,所以還需要加上對 pushState 的監(jiān)聽,以下是具體代碼示例及解析:
javascript
// hash路由監(jiān)聽
export function hashPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁面的時間
let beforePage = ''; // 上一個頁面
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime; //當(dāng)前時間 - 進(jìn)入時間
beforeTime = curTime;
return stayTime;
}
function listener() {
const stayTime = getStayTime();
const currentPage = window.location.href;
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// hash路由監(jiān)聽
window.addEventListener('hashchange', function () {
listener()
});
// 頁面load監(jiān)聽
window.addEventListener('load', function () {
listener()
});
const createHistoryEvent = function (name) {
const origin = window.history[name];
return function(event) {
//自定義事件
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
window.history.pushState = createHistoryEvent('pushState');
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
}
(五)UV 統(tǒng)計
UV 統(tǒng)計相對來說較為簡單,只需在 SDK 初始化時上報一條消息即可完成相關(guān)數(shù)據(jù)的收集。
四、數(shù)據(jù)上報方式
(一)xhr 接口請求
采用接口請求的方式來上報數(shù)據(jù),其原理和其他業(yè)務(wù)請求類似,只不過傳遞的數(shù)據(jù)是埋點相關(guān)的數(shù)據(jù)。但這種方式存在一些問題,一方面,通常公司里處理埋點的服務(wù)器和處理業(yè)務(wù)邏輯的服務(wù)器并非同一臺,所以往往需要手動去解決跨域問題;另一方面,如果在上報過程中出現(xiàn)頁面刷新或者重新打開頁面的情況,很可能會造成埋點數(shù)據(jù)的缺失,因此傳統(tǒng)的 xhr 接口請求方式在適應(yīng)埋點需求方面存在一定局限性。
(二)img 標(biāo)簽
利用 img 標(biāo)簽上報數(shù)據(jù),是將埋點數(shù)據(jù)偽裝成圖片 url 的請求形式,這樣做的好處是能夠避免跨域問題。然而,瀏覽器對 url 的長度是有限制的,所以這種方式不太適合大數(shù)據(jù)量的上報,而且同樣存在刷新或重新打開頁面時數(shù)據(jù)丟失的問題。
(三)sendBeacon
這種上報方式不會出現(xiàn)跨域問題,也不存在刷新或重新打開頁面導(dǎo)致的數(shù)據(jù)丟失情況,但它有兼容性方面的問題。在日常開發(fā)中,通常會采用 sendBeacon 上報和 img 標(biāo)簽上報相結(jié)合的方式,以此來兼顧各種情況,確保數(shù)據(jù)上報的有效性和穩(wěn)定性。以下是具體的上報函數(shù)代碼示例:
javascript
// 上報
export function report(data) {
const url = window['_monitor_report_url_'];
// ------- fetch方式上報 -------
// 跨域問題
// fetch(url, {
// method: 'POST',
// body: JSON.stringify(data),
// headers: {
// 'Content-Type': 'application/json',
// },
// }).then(res => {
// console.log(res);
// }).catch(err => {
// console.error(err);
// })
// ------- navigator/img方式上報 -------
// 不會有跨域問題
if (navigator.sendBeacon) { // 支持sendBeacon的瀏覽器
navigator.sendBeacon(url, JSON.stringify(data));
} else { // 不支持sendBeacon的瀏覽器
let oImage = new Image();
oImage.src = `${url}?logs=${data}`;
}
clearCache();
}
通過上述對前端實現(xiàn)監(jiān)控 SDK 的介紹,涵蓋了能拆分到數(shù)據(jù)上報環(huán)節(jié),旨在幫助開發(fā)人員更好地構(gòu)建和運用監(jiān)控系統(tǒng),以保障前端應(yīng)用的穩(wěn)定運行以及對用戶行為等方面的有效洞察。