前端頁面的監控是各個web項目必備的工具,本文将重點關注其中的錯誤部分,主要介紹一下常見的錯誤類型以及如何對它們進行捕獲并上報。
本文首發于 vivo網際網路技術 微信公衆号 https://mp.weixin.qq.com/s/E51lKQOojsvhHvACIyXwhw
作者:黃文佳
常見錯誤的分類
對于使用者在通路頁面時發生的錯誤,主要包括以下幾個類型:
1、js運作時錯誤
JavaScript代碼在使用者浏覽器中執行時,由于一些邊界情況、本地環境的不可控等因素,可能會存在js運作時錯誤。
而依賴用戶端的某些方法,由于相容性或者網絡等問題,也有機率會出現運作時錯誤。
e.g: 下圖是當使用了未定義的變量"foo",導緻産生js運作時錯誤時的上報資料:
2、資源加載錯誤
這裡的靜态資源包括js、css以及image等。現在的web項目,往往依賴了大量的靜态資源,而且一般也會有cdn存在。
如果某個節點出現問題導緻某個靜态資源無法通路,就需要能夠捕獲這種異常并進行上報,友善第一時間解決問題。
e.g: 下圖是圖檔資源不存在時的上報資料:
3、未處理的promise錯誤
未使用catch捕獲的promise錯誤,往往都會存在比較大的風險。而編碼時有可能覆寫的不夠全面,是以有必要監控未處理的promise錯誤并進行上報。
e.g: 下圖是promise請求接口發生錯誤後,未進行catch時的上報資料:
4、異步請求錯誤(fetch與xhr)
異步錯誤的捕獲分為兩個部分:一個是傳統的XMLHttpRequest,另一個是使用fetch api。
像axios和jQuery等庫就是在xhr上的封裝,而有些情況也可能會使用原生的fetch,是以對這兩種情況都要進行捕獲。
e.g: 下圖是xhr請求接口傳回400時捕獲後的上報資料:
各個類型錯誤的捕獲方式
1、window.onerror與window.addEventListener('error')捕獲js運作時錯誤
使用window.onerror和window.addEventListener('error')都能捕獲,但是window.onerror含有詳細的error堆棧資訊,存在error.stack中,是以我們選擇使用onerror的方式對js運作時錯誤進行捕獲。
window.onerror = function (msg, url, lineNo, columnNo, error) {
// 處理錯誤資訊
}
// demo
msg: Uncaught TypeError: Uncaught ReferenceError: a is not defined
error.statck: TypeError: ReferenceError: a is not defined at http://xxxx.js:1:13
window.addEventListener('error', event => (){
// 處理錯誤資訊
}, false);
// true代表在捕獲階段調用,false代表在冒泡階段捕獲。使用true或false都可以,預設為false
2、資源加載錯誤使用addEventListener去監聽error事件捕獲
實作原理:當一項資源(如<img>或<script>)加載失敗,加載資源的元素會觸發一個Event接口的error事件,并執行該元素上的onerror()處理函數。
這些error事件不會向上冒泡到window,不過能被window.addEventListener在捕獲階段捕獲。
但這裡需要注意,由于上面提到了addEventListener也能夠捕獲js錯誤,是以需要過濾避免重複上報,判斷為資源錯誤的時候才進行上報。
window.addEventListener('error', event => (){
// 過濾js error
let target = event.target || event.srcElement;
let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
if (!isElementTarget) return false;
// 上報資源位址
let url = target.src || target.href;
console.log(url);
}, true);
3、未處理的promise錯誤處理方式
實作原理:當promise被reject并且錯誤資訊沒有被處理的時候,會抛出一個unhandledrejection。
這個錯誤不會被window.onerror以及window.addEventListener('error')捕獲,但是有專門的window.addEventListener('unhandledrejection')方法進行捕獲處理。
window.addEventListener('rejectionhandled', event => {
// 錯誤的詳細資訊在reason字段
// demo:settimeout error
console.log(event.reason);
});
4、fetch與xhr錯誤的捕獲
對于fetch和xhr,我們需要通過改寫它們的原生方法,在觸發錯誤時進行自動化的捕獲和上報。
改寫fetch方法:
// fetch的處理
function _errorFetchInit () {
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) { // 當status不為2XX的時候,上報錯誤
}
return res;
})
// 當fetch方法錯誤時上報
.catch(error => {
// error.message,
// error.stack
// 抛出錯誤并且上報
throw error;
})
}
}
對于XMLHttpRequest的重寫:
xhr改寫
// xhr的處理
function _errorAjaxInit () {
let protocol = window.location.protocol;
if (protocol === 'file:') return;
// 處理XMLHttpRequest
if (!window.XMLHttpRequest) {
return;
}
let xmlhttp = window.XMLHttpRequest;
// 儲存原生send方法
let _oldSend = xmlhttp.prototype.send;
let _handleEvent = function (event) {
try {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
// event.currentTarget 即為建構的xhr執行個體
// event.currentTarget.response
// event.currentTarget.responseURL || event.currentTarget.ajaxUrl
// event.currentTarget.status
// event.currentTarget.statusText
});
}
} catch (e) {va
console.log('Tool\'s error: ' + e);
}
}
xmlhttp.prototype.send = function () {
this.addEventListener('error', _handleEvent); // 失敗
this.addEventListener('load', _handleEvent); // 完成
this.addEventListener('abort', _handleEvent); // 取消
return _oldSend.apply(this, arguments);
}
}
關于responseURL 的說明
需要特别注意的是,當請求完全無法執行的時候,XMLHttpRequest會收到status=0 和 statusText=null的傳回,此時responseURL也為空string。
另外在安卓4.4及以下版本的webview中,xhr對象也不存在responseURL屬性。
是以我們需要額外的改寫xhr的open方法,将傳入的url記錄下來,友善上報時帶上。
var _oldOpen = xmlhttp.prototype.open;
// 重寫open方法,記錄請求的url
xmlhttp.prototype.open = function (method, url) {
_oldOpen.apply(this, arguments);
this.ajaxUrl = url;
};
其他問題
1、其他架構,例如vue項目的錯誤捕獲
vue内部發生的錯誤會被Vue攔截,是以vue提供方法給我們處理vue元件内部發生的錯誤。
Vue.config.errorHandler = function (err, vm, info) {
2、script error的解決方式
"script error.”有時也被稱為跨域錯誤。當網站請求并執行一個托管在第三方域名下的腳本時,就可能遇到該錯誤。最常見的情形是使用 CDN 托管 JS 資源。
其實這并不是一個 JavaScript Bug。出于安全考慮,浏覽器會刻意隐藏其他域的 JS 檔案抛出的具體錯誤資訊,這樣做可以有效避免敏感資訊無意中被不受控制的第三方腳本捕獲。
是以,浏覽器隻允許同域下的腳本捕獲具體錯誤資訊,而其他腳本隻知道發生了一個錯誤,但無法獲知錯誤的具體内容。
解決方案1:(推薦)
添加 crossorigin="anonymous" 屬性。
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
此步驟的作用是告知浏覽器以匿名方式擷取目标腳本。這意味着請求腳本時不會向服務端發送潛在的使用者身份資訊(例如 Cookies、HTTP 證書等)。
添加跨域 HTTP 響應頭:
Access-Control-Allow-Origin: *
或者
Access-Control-Allow-Origin: http://test.com
**注意:**大部分主流 CDN 預設添加了 Access-Control-Allow-Origin 屬性。
完成上述兩步之後,即可通過 window.onerror 捕獲跨域腳本的報錯資訊。
解決方案2
難以在 HTTP 請求響應頭中添加跨域屬性時,還可以考慮 try catch 這個備選方案。
在如下示例 HTML 頁面中加入 try catch:
<!doctype html>
<html>
<head>
<title>Test page in http://test.com</title>
</head>
<body>
<script src="http://another-domain.com/app.js"></script>
// app.js裡面有一個foo方法,調用了不存在的bar方法
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}
try {
foo();
} catch (e) {
console.log(e);
throw e;
}
</script>
</body>
</html>
// 運作輸出結果如下:
=> ReferenceError: bar is not defined
at foo (http://another-domain.com/app.js:2:3)
at http://test.com/:15:3
=> "Script error.", "", 0, 0, undefined
可見 try catch 中的 Console 語句輸出了完整的資訊,但 window.onerror 中隻能捕獲“Script error”。根據這個特點,可以在 catch 語句中手動上報捕獲的異常。
總結
上述的錯誤捕獲基本覆寫了前端監控所需的錯誤場景,但是第三部分指出的兩個其他問題,目前解決的方式都不太完美。
對于有使用架構的項目:一是需要有額外的處理流程,比如示例中就需要單獨為vue項目進行初始化;二是對于其他架構,都需要單獨處理,例如react項目的話,則需要使用官方提供的componentDidCatch方法來做錯誤捕獲。
而對于跨域js捕獲的問題:我們并不能保證所有的跨域靜态資源都添加跨域 HTTP 響應頭;而通過第二種包裹try-catch的方式進行上報,則需要考慮的場景繁多并且無法保證沒有遺漏。
雖然存在這兩點不足,但前端錯誤捕獲這部分還是和項目的使用場景密切相關的。我們可以在了解這些方式以後,選擇最适合自己項目的方案,為自己的監控工具服務。
—— —— 參考文檔 —— ——
1.Using XMLHttpRequest:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
2.script error 産生的原因和解決辦法:
https://www.alibabacloud.com/help/zh/faq-detail/88579.htm
3.JavaScript執行錯誤:
https://docs.fundebug.com/notifier/javascript/type/javascript.html
4.betterjs的script error:
https://github.com/BetterJS/badjs-report/issues/3
5.Vuejs的errorHandler:
https://cn.vuejs.org/v2/api/index.html#errorHandler
6.React的componentDidCatch:
https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html
更多内容敬請關注 vivo 網際網路技術 微信公衆号
注:轉載文章請先與微信号:labs2020 聯系。
分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。