前言
做好錯(cuò)誤監(jiān)控面哼,將用戶使用時(shí)的錯(cuò)誤日志上報(bào)哼绑,可以幫助我們更快的解決一些問題宗苍。目前開源的比較好的前端監(jiān)控有
那前端監(jiān)控是怎么實(shí)現(xiàn)的呢?要想了解這個(gè)苔咪,需要知道前端錯(cuò)誤大概分為哪些以及如何捕獲處理搀突。
前端錯(cuò)誤分為JS運(yùn)行時(shí)錯(cuò)誤刀闷、資源加載錯(cuò)誤和接口錯(cuò)誤三種。
一、JS運(yùn)行時(shí)錯(cuò)誤
JS運(yùn)行時(shí)錯(cuò)誤一般使用window.onerror捕獲甸昏,但是有一種特殊情況就是promise被reject并且錯(cuò)誤信息沒有被處理的時(shí)候拋出的錯(cuò)誤
1.1 一般情況的JS運(yùn)行時(shí)錯(cuò)誤
使用window.onerror和window.addEventListener('error')捕獲顽分。
window.onerror = function (msg, url, lineNo, columnNo, error)
{
// 處理error信息
}
window.addEventListener('error', event =>
{
console.log('addEventListener error:' + event.target);
}, true);
// true代表在捕獲階段調(diào)用,false代表在冒泡階段捕獲施蜜。使用true或false都可以
例子:https://jsbin.com/lujahin/edit?html,console,output 點(diǎn)擊button拋出錯(cuò)誤卒蘸,分別被window.onerror和window.addEventListener('error')捕獲
1.2 Uncaught (in promise)
當(dāng)promise被reject并且錯(cuò)誤信息沒有被處理的時(shí)候,會(huì)拋出一個(gè)unhandledrejection翻默,并且這個(gè)錯(cuò)誤不會(huì)被window.onerror以及window.addEventListener('error')捕獲缸沃,需要用專門的window.addEventListener('unhandledrejection')捕獲處理
window.addEventListener('unhandledrejection', event =>
{
console.log('unhandledrejection:' + event.reason); // 捕獲后自定義處理
});
https://developer.mozilla.org...
例子:https://jsbin.com/jofomob/edit?html,console,output 點(diǎn)擊button拋出unhandledrejection錯(cuò)誤,并且該錯(cuò)誤僅能被window.addEventListener('unhandledrejection')捕獲
1.3 console.error
一些特殊情況下修械,還需要捕獲處理console.error趾牧,捕獲方式就是重寫window.console.error
var consoleError = window.console.error;
window.console.error = function () {
alert(JSON.stringify(arguments)); // 自定義處理
consoleError && consoleError.apply(window, arguments);
};
1.4 特別說明跨域日志
什么是跨域腳本error?
https://developer.mozilla.org...
當(dāng)加載自不同域的腳本中發(fā)生語法錯(cuò)誤時(shí)肯污,為避免信息泄露(參見bug 363897)翘单,語法錯(cuò)誤的細(xì)節(jié)將不會(huì)報(bào)告,而代之簡(jiǎn)單的"Script error."仇箱。在某些瀏覽器中县恕,通過在<script>使用crossorigin屬性并要求服務(wù)器發(fā)送適當(dāng)?shù)?CORS HTTP 響應(yīng)頭,該行為可被覆蓋剂桥。一個(gè)變通方案是單獨(dú)處理"Script error.",告知錯(cuò)誤詳情僅能通過瀏覽器控制臺(tái)查看属提,無法通過JavaScript訪問权逗。
window.onerror = function (msg, url, lineNo, columnNo, error) {
var string = msg.toLowerCase();
var substring = "script error";
if (string.indexOf(substring) > -1){
alert('Script Error: See Browser Console for Detail');
} else {
var message = [
'Message: ' + msg,
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
alert(message);
}
return false;
};
例子: http://sandbox.runjs.cn/show/... 請(qǐng)打開頁(yè)面打開控制臺(tái)。該頁(yè)面分別加載了兩個(gè)不同域的js腳本冤议,配置了crossorigin的window.onerror可以報(bào)出詳細(xì)的錯(cuò)誤斟薇,沒有配置crossorigin只能報(bào)出'script error',并且沒有錯(cuò)誤信息
為了跨域捕獲JavaScript異常恕酸,可執(zhí)行以下兩個(gè)解法:
解法一堪滨、1.添加crossorigin="anonymous"屬性。
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
此步驟的作用是告知瀏覽器以匿名方式獲取目標(biāo)腳本蕊温。這意味著請(qǐng)求腳本時(shí)不會(huì)向服務(wù)端發(fā)送潛在的用戶身份信息(例如Cookies袱箱、HTTP證書等)。
2.添加跨域HTTP響應(yīng)頭义矛。
Access-Control-Allow-Origin: *
或者 Access-Control-Allow-Origin: http://test.com
解法二 发笔、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>
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}
try {
foo(); // 調(diào)用app.js中定義的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
可見try catch
中的Console語句輸出了完整的信息凉翻,但window.onerror
中只能捕獲“Script error”了讨。根據(jù)這個(gè)特點(diǎn),可以在catch語句中手動(dòng)上報(bào)捕獲的異常,詳情請(qǐng)參見API 使用指南前计。
1.5 特別說明sourceMap
在線上由于JS一般都是被壓縮或者打包(webpack)過胞谭,打包后的文件只有一行,因此報(bào)錯(cuò)會(huì)出現(xiàn)第一行第5000列出現(xiàn)JS錯(cuò)誤男杈,給排查帶來困難韭赘。sourceMap存儲(chǔ)打包前的JS文件和打包后的JS文件之間一個(gè)映射關(guān)系,可以根據(jù)打包后的位置快速解析出對(duì)應(yīng)源文件的位置势就。
但是出于安全性考慮泉瞻,線上設(shè)置sourceMap會(huì)存在不安全的問題,因?yàn)榫W(wǎng)站使用者可以輕易的看到網(wǎng)站源碼苞冯,此時(shí)可以設(shè)置.map文件只能通過公司內(nèi)網(wǎng)訪問降低隱患
sourceMap配置devtool: 'inline-source-map'
如果使用了uglifyjs-webpack-plugin 必須把 sourceMap設(shè)置為true
https://doc.webpack-china.org...
1.6 其它
1.6.1 sentry把所有的回調(diào)函數(shù)使用try catch封裝一層
https://github.com/getsentry/raven-js/blob/master/src/raven.js
1.6.2 vue errorHandler
https://vuejs.org/v2/api/#errorHandler
其原理也是使用try catch封裝了nextTick,$emit, watch,data等
https://github.com/vuejs/vue/blob/dev/dist/vue.runtime.js
二袖牙、資源加載錯(cuò)誤
使用window.addEventListener('error')捕獲,window.onerror捕獲不到資源加載錯(cuò)誤
https://jsbin.com/rigasek/edit?html,console 圖片資源加載錯(cuò)誤舅锄。此時(shí)只有window.addEventListener('error')可以捕獲到
window.onerror和window.addEventListener('error')的異同:相同點(diǎn)是都可以捕獲到window上的js運(yùn)行時(shí)錯(cuò)誤鞭达。區(qū)別是1.捕獲到的錯(cuò)誤參數(shù)不同 2.window.addEventListener('error')可以捕獲資源加載錯(cuò)誤,但是window.onerror不能捕獲到資源加載錯(cuò)誤
window.addEventListener('error')捕獲到的錯(cuò)誤皇忿,可以通過target?.src || target?.href區(qū)分是資源加載錯(cuò)誤還是js運(yùn)行時(shí)錯(cuò)誤
三畴蹭、接口錯(cuò)誤
所有http請(qǐng)求都是基于xmlHttpRequest或者fetch封裝的。所以要捕獲全局的接口錯(cuò)誤鳍烁,方法就是封裝xmlHttpRequest或者fetch
3.1 封裝xmlHttpRequest
if(!window.XMLHttpRequest) return;
var xmlhttp = window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
// 自定義錯(cuò)誤上報(bào) }
}
xmlhttp.prototype.send = function () {
if (this['addEventListener']) {
this['addEventListener']('error', _handleEvent);
this['addEventListener']('load', _handleEvent);
this['addEventListener']('abort', _handleEvent);
} else {
var _oldStateChange = this['onreadystatechange'];
this['onreadystatechange'] = function (event) {
if (this.readyState === 4) {
_handleEvent(event);
}
_oldStateChange && _oldStateChange.apply(this, arguments);
};
}
return _oldSend.apply(this, arguments);
}
3.2 封裝fetch
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) { // True if status is HTTP 2xx
// 上報(bào)錯(cuò)誤
}
return res;
})
.catch(error => {
// 上報(bào)錯(cuò)誤
throw error;
})
}
結(jié)論
- 使用window.onerror捕獲JS運(yùn)行時(shí)錯(cuò)誤
- 使用window.addEventListener('unhandledrejection')捕獲未處理的promise reject錯(cuò)誤
- 重寫console.error捕獲console.error錯(cuò)誤
- 在跨域腳本上配置
crossorigin="anonymous"
捕獲跨域腳本錯(cuò)誤 - window.addEventListener('error')捕獲資源加載錯(cuò)誤叨襟。因?yàn)樗材懿东@js運(yùn)行時(shí)錯(cuò)誤,為避免重復(fù)上報(bào)js運(yùn)行時(shí)錯(cuò)誤幔荒,此時(shí)只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement時(shí)才上報(bào)
- 重寫window.XMLHttpRequest和window.fetch捕獲請(qǐng)求錯(cuò)誤
利用以上原理糊闽,簡(jiǎn)單寫了一個(gè)JS監(jiān)控,只處理了一些JS錯(cuò)誤爹梁,暫時(shí)沒有做和性能相關(guān)的監(jiān)控
https://github.com/Lie8466/better-js
如果發(fā)現(xiàn)文章有錯(cuò)誤右犹,歡迎指正。