您好,登錄后才能下訂單哦!
本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào) https://mp.weixin.qq.com/s/E51lKQOojsvhHvACIyXwhw
作者:黃文佳
對(duì)于用戶在訪問(wèn)頁(yè)面時(shí)發(fā)生的錯(cuò)誤,主要包括以下幾個(gè)類型:
JavaScript代碼在用戶瀏覽器中執(zhí)行時(shí),由于一些邊界情況、本地環(huán)境的不可控等因素,可能會(huì)存在js運(yùn)行時(shí)錯(cuò)誤。
而依賴客戶端的某些方法,由于兼容性或者網(wǎng)絡(luò)等問(wèn)題,也有概率會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤。
e.g: 下圖是當(dāng)使用了未定義的變量"foo",導(dǎo)致產(chǎn)生js運(yùn)行時(shí)錯(cuò)誤時(shí)的上報(bào)數(shù)據(jù):
這里的靜態(tài)資源包括js、css以及image等?,F(xiàn)在的web項(xiàng)目,往往依賴了大量的靜態(tài)資源,而且一般也會(huì)有cdn存在。
如果某個(gè)節(jié)點(diǎn)出現(xiàn)問(wèn)題導(dǎo)致某個(gè)靜態(tài)資源無(wú)法訪問(wèn),就需要能夠捕獲這種異常并進(jìn)行上報(bào),方便第一時(shí)間解決問(wèn)題。
e.g: 下圖是圖片資源不存在時(shí)的上報(bào)數(shù)據(jù):
未使用catch捕獲的promise錯(cuò)誤,往往都會(huì)存在比較大的風(fēng)險(xiǎn)。而編碼時(shí)有可能覆蓋的不夠全面,因此有必要監(jiān)控未處理的promise錯(cuò)誤并進(jìn)行上報(bào)。
e.g: 下圖是promise請(qǐng)求接口發(fā)生錯(cuò)誤后,未進(jìn)行catch時(shí)的上報(bào)數(shù)據(jù):
異步錯(cuò)誤的捕獲分為兩個(gè)部分:一個(gè)是傳統(tǒng)的XMLHttpRequest,另一個(gè)是使用fetch api。
像axios和jQuery等庫(kù)就是在xhr上的封裝,而有些情況也可能會(huì)使用原生的fetch,因此對(duì)這兩種情況都要進(jìn)行捕獲。
e.g: 下圖是xhr請(qǐng)求接口返回400時(shí)捕獲后的上報(bào)數(shù)據(jù):
使用window.onerror和window.addEventListener('error')都能捕獲,但是window.onerror含有詳細(xì)的error堆棧信息,存在error.stack中,所以我們選擇使用onerror的方式對(duì)js運(yùn)行時(shí)錯(cuò)誤進(jìn)行捕獲。
window.onerror = function (msg, url, lineNo, columnNo, error) {
// 處理錯(cuò)誤信息
}
// 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 => (){
// 處理錯(cuò)誤信息
}, false);
// true代表在捕獲階段調(diào)用,false代表在冒泡階段捕獲。使用true或false都可以,默認(rèn)為false
實(shí)現(xiàn)原理:當(dāng)一項(xiàng)資源(如<img>或<script>)加載失敗,加載資源的元素會(huì)觸發(fā)一個(gè)Event接口的error事件,并執(zhí)行該元素上的onerror()處理函數(shù)。
這些error事件不會(huì)向上冒泡到window,不過(guò)能被window.addEventListener在捕獲階段捕獲。
但這里需要注意,由于上面提到了addEventListener也能夠捕獲js錯(cuò)誤,因此需要過(guò)濾避免重復(fù)上報(bào),判斷為資源錯(cuò)誤的時(shí)候才進(jìn)行上報(bào)。
window.addEventListener('error', event => (){
// 過(guò)濾js error
let target = event.target || event.srcElement;
let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
if (!isElementTarget) return false;
// 上報(bào)資源地址
let url = target.src || target.href;
console.log(url);
}, true);
實(shí)現(xiàn)原理:當(dāng)promise被reject并且錯(cuò)誤信息沒(méi)有被處理的時(shí)候,會(huì)拋出一個(gè)unhandledrejection。
這個(gè)錯(cuò)誤不會(huì)被window.onerror以及window.addEventListener('error')捕獲,但是有專門的window.addEventListener('unhandledrejection')方法進(jìn)行捕獲處理。
window.addEventListener('rejectionhandled', event => {
// 錯(cuò)誤的詳細(xì)信息在reason字段
// demo:settimeout error
console.log(event.reason);
});
對(duì)于fetch和xhr,我們需要通過(guò)改寫(xiě)它們的原生方法,在觸發(fā)錯(cuò)誤時(shí)進(jìn)行自動(dòng)化的捕獲和上報(bào)。
改寫(xiě)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) { // 當(dāng)status不為2XX的時(shí)候,上報(bào)錯(cuò)誤
}
return res;
})
// 當(dāng)fetch方法錯(cuò)誤時(shí)上報(bào)
.catch(error => {
// error.message,
// error.stack
// 拋出錯(cuò)誤并且上報(bào)
throw error;
})
}
}
對(duì)于XMLHttpRequest的重寫(xiě):
xhr改寫(xiě)
// 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 即為構(gòu)建的xhr實(shí)例
// 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);
}
}
關(guān)于responseURL 的說(shuō)明
需要特別注意的是,當(dāng)請(qǐng)求完全無(wú)法執(zhí)行的時(shí)候,XMLHttpRequest會(huì)收到status=0 和 statusText=null的返回,此時(shí)responseURL也為空string。
另外在安卓4.4及以下版本的webview中,xhr對(duì)象也不存在responseURL屬性。
因此我們需要額外的改寫(xiě)xhr的open方法,將傳入的url記錄下來(lái),方便上報(bào)時(shí)帶上。
var _oldOpen = xmlhttp.prototype.open;
// 重寫(xiě)open方法,記錄請(qǐng)求的url
xmlhttp.prototype.open = function (method, url) {
_oldOpen.apply(this, arguments);
this.ajaxUrl = url;
};
vue內(nèi)部發(fā)生的錯(cuò)誤會(huì)被Vue攔截,因此vue提供方法給我們處理vue組件內(nèi)部發(fā)生的錯(cuò)誤。
Vue.config.errorHandler?=?function?(err,?vm,?info)?{
"script error.”有時(shí)也被稱為跨域錯(cuò)誤。當(dāng)網(wǎng)站請(qǐng)求并執(zhí)行一個(gè)托管在第三方域名下的腳本時(shí),就可能遇到該錯(cuò)誤。最常見(jiàn)的情形是使用 CDN 托管 JS 資源。
其實(shí)這并不是一個(gè) JavaScript Bug。出于安全考慮,瀏覽器會(huì)刻意隱藏其他域的 JS 文件拋出的具體錯(cuò)誤信息,這樣做可以有效避免敏感信息無(wú)意中被不受控制的第三方腳本捕獲。
因此,瀏覽器只允許同域下的腳本捕獲具體錯(cuò)誤信息,而其他腳本只知道發(fā)生了一個(gè)錯(cuò)誤,但無(wú)法獲知錯(cuò)誤的具體內(nèi)容。
添加 crossorigin="anonymous" 屬性。
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
此步驟的作用是告知瀏覽器以匿名方式獲取目標(biāo)腳本。這意味著請(qǐng)求腳本時(shí)不會(huì)向服務(wù)端發(fā)送潛在的用戶身份信息(例如 Cookies、HTTP 證書(shū)等)。
添加跨域 HTTP 響應(yīng)頭:
Access-Control-Allow-Origin: *
或者
Access-Control-Allow-Origin: http://test.com
注意:大部分主流 CDN 默認(rèn)添加了 Access-Control-Allow-Origin 屬性。
完成上述兩步之后,即可通過(guò) window.onerror 捕獲跨域腳本的報(bào)錯(cuò)信息。
難以在 HTTP 請(qǐng)求響應(yīng)頭中添加跨域?qū)傩詴r(shí),還可以考慮 try catch 這個(gè)備選方案。
在如下示例 HTML 頁(yè)面中加入 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里面有一個(gè)foo方法,調(diào)用了不存在的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>
// 運(yùn)行輸出結(jié)果如下:
=> 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
可見(jiàn) try catch 中的 Console 語(yǔ)句輸出了完整的信息,但 window.onerror 中只能捕獲“Script error”。根據(jù)這個(gè)特點(diǎn),可以在 catch 語(yǔ)句中手動(dòng)上報(bào)捕獲的異常。
上述的錯(cuò)誤捕獲基本覆蓋了前端監(jiān)控所需的錯(cuò)誤場(chǎng)景,但是第三部分指出的兩個(gè)其他問(wèn)題,目前解決的方式都不太完美。
對(duì)于有使用框架的項(xiàng)目:一是需要有額外的處理流程,比如示例中就需要單獨(dú)為vue項(xiàng)目進(jìn)行初始化;二是對(duì)于其他框架,都需要單獨(dú)處理,例如react項(xiàng)目的話,則需要使用官方提供的componentDidCatch方法來(lái)做錯(cuò)誤捕獲。
而對(duì)于跨域js捕獲的問(wèn)題:我們并不能保證所有的跨域靜態(tài)資源都添加跨域 HTTP 響應(yīng)頭;而通過(guò)第二種包裹try-catch的方式進(jìn)行上報(bào),則需要考慮的場(chǎng)景繁多并且無(wú)法保證沒(méi)有遺漏。
雖然存在這兩點(diǎn)不足,但前端錯(cuò)誤捕獲這部分還是和項(xiàng)目的使用場(chǎng)景密切相關(guān)的。我們可以在了解這些方式以后,選擇最適合自己項(xiàng)目的方案,為自己的監(jiān)控工具服務(wù)。
—— —— 參考文檔 —— ——
1.Using XMLHttpRequest:
_https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_
2.script error 產(chǎn)生的原因和解決辦法:
https://www.alibabacloud.com/help/zh/faq-detail/88579.htm
3.JavaScript執(zhí)行錯(cuò)誤:
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
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。