當有人問起:你們的公司的這款應用使用者體驗怎麼樣呀?訪問量怎麼樣?此時,你該怎麼回答呢?你會回答:UV、PV 巴拉巴拉,秒開率、FP、TTI 巴拉巴拉。
那麼,這些資料是哪裡來的呢?顯而易見,這些資料都來自前端監控系統。
前端監控的意義
當今時代,是一個快節奏的時代,應用的效能極大影響著使用者的留存率,沒有使用者會忍受一個卡到爆的應用。而監控應用效能的重擔,就由前端監控系統肩負著。
其次,對於線上應用來說,故障是不可避免的,對於高日活的應用來說,每次故障都意味著大量的損失。試想,如果是淘寶掛了一天,那麼損失是多麼慘痛。所以,對於開發人員來說,必須要儘早發現線上故障,而不是等到客戶打爆客服的電話才發現。線上錯誤監控,也是前端監控的任務之一。
最後,作為商業公司,需要根據使用者行為和資料進行分析,進一步制定各種策略,如果沒有各種資料,那麼 BI 會熱情的找你談談人生。而這些資料,也是前端監控系統獲取的。
總而言之,前端監控肩負著:效能監控、錯誤監控以及資料上報等功能,無論對於大公司還是小公司,可以說是必不可缺的了。
今天,我們先來聊聊前端監控中的錯誤監控。
錯誤監控概述
一般來說,按照錯誤監控錯誤監控可以分為:指令碼錯誤監控、請求錯誤監控以及資源錯誤監控。
指令碼錯誤監控
指令碼錯誤大體可以分為兩種:編譯時錯誤以及執行時錯誤。其中,編譯時錯誤一般在開發階段就會發現,配合 lint 工具比如 eslint、tslint 等以及 git 提交外掛比如 husky 等,基本可以保證線上程式碼不出現低階的編譯時錯誤。大廠一般都有釋出前置檢測平臺,能夠在釋出前提前發現編譯時錯誤,當然,原理依然和之前所說的類似。
而發現並上報執行時錯誤就是前端檢測平臺的本質工作啦,一般來說,指令碼錯誤監控指的就是執行時錯誤監控。
說到指令碼錯誤監控,你想到的第一個是什麼?對,就是 try catch
!
在編寫 JavaScript 時,我們為了防止出現錯誤阻塞程式,我們會通過 try catch
捕獲錯誤,對於錯誤捕獲,這是最簡單也是最通用的方案。
但是,try catch
捕獲錯誤是侵入式的,需要在開發程式碼時即提前進行處理,而作為一個監控系統,無法做到在所有可能產生錯誤的程式碼片段中都嵌入 try catch
。所以,我們需要全域性捕獲指令碼錯誤。
常規指令碼錯誤
當頁面出現指令碼錯誤時,就會產生 onerror
事件,我們只需捕獲該事件即可。
/**
* @description window.onerror 全域性捕獲錯誤
* @param event 錯誤資訊,如果是
* @param source 錯誤原始檔URL
* @param lineno 行號
* @param colno 列號
* @param error Error物件
*/
window.onerror = function (event, source, lineno, colno, error) {
// 上報錯誤
// 如果不想在控制檯丟擲錯誤,只需返回 true 即可
};
可以發現,各種錯誤監控所需的資訊,如錯誤資訊、錯誤原始檔的 URL、錯誤行號、錯誤列號都被回撥函式所傳入。
但是,window.onload
有兩個缺點:
- 只能繫結一個回撥函式,如果想在不同檔案中想繫結不同的回撥函式,
window.onload
顯然無法完成;同時,不同回撥函式直接容易造成互相覆蓋。 - 回撥函式的引數過於離散,使用不方便
所以,一般情況下,我們使用 addEventListener
來代替。
/**
* @param event 事件名
* @param function 回撥函式
* @param useCapture 回撥函式是否在捕獲階段執行,預設是false,在冒泡階段執行
*/
window.addEventListener('error', (event) => {
// addEventListener 回撥函式的離散引數全部聚合在 error 物件中
// 上報錯誤
}, true)
tips:在一些特殊情況下,我們依然需要使用 window.onload
。比如,不期望在控制檯丟擲錯誤時,因為只有 window.onload
才能阻止丟擲錯誤到控制檯
Promise 錯誤
使用了這兩種方法,是不是可以捕獲所有指令碼錯誤了呢?這個問題再幾年前其實是正確的,但是隨著前端技術的發展,出現了 Promise
這項技術,而使用這兩種常規方法無法捕獲 Promise
錯誤。
和常規指令碼錯誤的捕獲一樣,我們只需捕獲 Promise
對應的錯誤事件即可。而 Promise
錯誤事件有兩種,unhandledrejection
以及 rejectionhandled
。
當 Promise
被 reject 且沒有 reject 處理器的時候,會觸發 unhandledrejection
事件。
當 Promise
被 reject 且有 reject 處理器的時候,會觸發 rejectionhandled
事件。
// unhandledrejection 推薦處理方案
window.addEventListener('unhandledrejection', (event) => {
console.log(event)
}, true);
// unhandledrejection 備選處理方案
window.onunhandledrejection = function (error) {
console.log(error)
}
// rejectionhandled 推薦處理方案
window.addEventListener('rejectionhandled', (event) => {
console.log(event)
}, true);
// rejectionhandled 備選處理方案
window.onrejectionhandled = function (error) {
console.log(error)
}
框架錯誤
由於我 React 使用的不多,所以在此只討論下 Vue 的框架錯誤處理,如果有大佬瞭解 React 的框架錯誤處理,歡迎補充~
在 Vue 中,框架提供了 errorHandler 這個 API 來捕獲並處理錯誤。
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
// 只在 2.2.0+ 可用
}
值得一提的是,框架錯誤指的不是框架層面的錯誤,而是指框架提供了 API 來捕獲全域性錯誤。
請求錯誤監控
一般來說,前端請求有兩種方案,使用 ajax
或者 fetch
,所以只需重寫兩種方法,進行代理,即可實現請求錯誤監控。
代理的核心在於使用 apply
重新執行原有方法,並且在執行原有方法之前進行監聽操作。在請求錯誤監控中,我們關心三種錯誤事件:abort
,error 以及 timeout
,所以,只需在代理中對這三種事件進行統一處理即可。
tips:如果能夠統一使用一種請求工具,如 axios
等,那麼不需要重寫 ajax
或者 fetch
只需在請求攔截器以及響應攔截器進行處理上報即可
資源錯誤監控
資源錯誤監控本質上和常規指令碼錯誤監控一樣,都是監控錯誤事件實現錯誤捕獲。
那麼如果區分指令碼錯誤還是資源錯誤呢?我們可以通過 instanceof
區分,指令碼錯誤引數物件 instanceof
ErrorEvent
,而資源錯誤的引數物件 instanceof
Event
。
值得一提的是,由於 ErrorEvent
繼承於 Event
,所以不管是指令碼錯誤還是資源錯誤的引數物件,它們都 instanceof
Event
,所以,需要先判斷指令碼錯誤。
此外,兩個引數物件之間有一些細微的不同,比如,指令碼錯誤的引數物件中包含 message
,而資源錯誤沒有,這些都可以作為判斷資源錯誤或者指令碼錯誤的依據。
/**
* @param event 事件名
* @param function 回撥函式
* @param useCapture 回撥函式是否在捕獲階段執行,預設是false,在冒泡階段執行
*/
window.addEventListener('error', (event) => {
if (event instanceof ErrorEvent) {
console.log('指令碼錯誤')
} else if (event instanceof Event) {
console.log('資源錯誤')
}
}, true);
tips:使用 addEventListener
捕獲資源錯誤時,一定要將 useCapture 即第三個選項設為 true,因為資源錯誤沒有冒泡,所以只能在捕獲階段捕獲。同理,由於 window.onerror
是通過在冒泡階段捕獲錯誤,所以無法捕獲資源錯誤。
補充:跨域指令碼錯誤捕獲
為了效能方面的考慮,我們一般會將指令碼檔案放到 CDN ,這種方法會大大加快首屏時間。但是,如果指令碼報錯,此時,瀏覽器出於於安全方面的考慮,對於不同源的指令碼報錯,無法捕獲到詳細錯誤資訊,只會顯示 Script Error
。那麼,有解決該問題的方案嗎?
- 方案一:所有的指令碼全部放到同一源下,但是,該方案會放棄
CDN
,降低效能。 - 方案二:在
script
標籤中,新增crossorigin
屬性(推薦使用webpack
外掛自動新增);同時,配置CDN
伺服器,為跨域指令碼配上CORS
。
可以發現,方案二基本可以完美解決跨域指令碼錯誤捕獲的問題。但是,其實該方案有一個隱藏的坑,即相容性問題,crossorigin
屬性對於 IE 以及 Safari 支援程度不高。
所以,該如何真正完美的解決跨域指令碼錯誤捕獲問題?
終極解決方案:對所有原生方法進行代理~
但是,一方面,很難覆蓋所有的原生方法,另一方面,對原生方法進行代理容易出現無法預知的問題。
綜合所有方案,看起來還是方案二最靠譜,至於低階瀏覽器,就讓它們隨風消逝吧~
如果有不同想法的同學,歡迎一起交流~
我的 github:github.com/KarthusLori…