|
https://www.cnblogs.com/datiangou/p/10224846.html
寫在前面
在前端項(xiàng)目中,由于JavaScript本身是一個(gè)弱類型語言,加上瀏覽器環(huán)境的復(fù)雜性,網(wǎng)絡(luò)問題等等,很容易發(fā)生錯(cuò)誤。做好網(wǎng)頁錯(cuò)誤監(jiān)控,不斷優(yōu)化代碼,提高代碼健壯性是一項(xiàng)很重要的工作。本文將從Error開始,講到如何捕獲頁面中的異常。文章較長,細(xì)節(jié)較多,請耐心觀看。
前端開發(fā)中的Error
JavaScript中的Error
JavaScript中,Error是一個(gè)構(gòu)造函數(shù),通過它創(chuàng)建一個(gè)錯(cuò)誤對象。當(dāng)運(yùn)行時(shí)錯(cuò)誤產(chǎn)生時(shí),Error的實(shí)例對象會(huì)被拋出。構(gòu)造一個(gè)Error的語法如下:
// message: 錯(cuò)誤描述
// fileName: 可選。被創(chuàng)建的Error對象的fileName屬性值。默認(rèn)是調(diào)用Error構(gòu)造器代碼所在的文件的名字。
// lineNumber: 可選。被創(chuàng)建的Error對象的lineNumber屬性值。默認(rèn)是調(diào)用Error構(gòu)造器代碼所在的文件的行號。
new Error([message[, fileName[, lineNumber]]])
ECMAScript標(biāo)準(zhǔn):
Error有兩個(gè)標(biāo)準(zhǔn)屬性:
Error.prototype.name?:錯(cuò)誤的名字
Error.prototype.message:錯(cuò)誤的描述
例如,在chrome控制臺(tái)中輸入以下代碼:
var a = new Error('錯(cuò)誤測試');
console.log(a); // Error: 錯(cuò)誤測試
// at <anonymous>:1:9
console.log(a.name); // Error
console.log(a.message); // 錯(cuò)誤測試
Error只有一個(gè)標(biāo)準(zhǔn)方法:
Error.prototype.toString:返回表示一個(gè)表示錯(cuò)誤的字符串。
接上面的代碼:
a.toString(); // "Error: 錯(cuò)誤測試"
非標(biāo)準(zhǔn)的屬性
各個(gè)瀏覽器廠商對于Error都有自己的實(shí)現(xiàn)。比如下面這些屬性:
Error.prototype.fileName:產(chǎn)生錯(cuò)誤的文件名。
Error.prototype.lineNumber:產(chǎn)生錯(cuò)誤的行號。
Error.prototype.columnNumber:產(chǎn)生錯(cuò)誤的列號。
Error.prototype.stack:堆棧信息。這個(gè)比較常用。
這些屬性均不是標(biāo)準(zhǔn)屬性,在生產(chǎn)環(huán)境中謹(jǐn)慎使用。不過現(xiàn)代瀏覽器差不多都支持了。
Error的種類
除了通用的Error構(gòu)造函數(shù)外,JavaScript還有7個(gè)其他類型的錯(cuò)誤構(gòu)造函數(shù)。
- InternalError: 創(chuàng)建一個(gè)代表Javascript引擎內(nèi)部錯(cuò)誤的異常拋出的實(shí)例。 如: "遞歸太多"。非ECMAScript標(biāo)準(zhǔn)。
- RangeError: 數(shù)值變量或參數(shù)超出其有效范圍。例子:var a = new Array(-1);
- EvalError: 與eval()相關(guān)的錯(cuò)誤。eval()本身沒有正確執(zhí)行。
- ReferenceError: 引用錯(cuò)誤。 例子:console.log(b);
- SyntaxError: 語法錯(cuò)誤。例子:var a = ;
- TypeError: 變量或參數(shù)不屬于有效范圍。例子:[1,2].split('.')
- URIError: 給 encodeURI或 decodeURl()傳遞的參數(shù)無效。例子:decodeURI('%2')
當(dāng)JavaScript運(yùn)行過程中出錯(cuò)時(shí),會(huì)拋出上8種(上述7種加上通用錯(cuò)誤類型)錯(cuò)誤中的其中一種錯(cuò)誤。錯(cuò)誤類型可以通過error.name拿到。
你也可以基于Error構(gòu)造自己的錯(cuò)誤類型,這里就不展開了。
其他錯(cuò)誤
上面介紹的都是JavaScript本身運(yùn)行時(shí)會(huì)發(fā)生的錯(cuò)誤。頁面中還會(huì)有其他的異常,比如錯(cuò)誤地操作了DOM。
DOMException
DOMException是W3C DOM核心對象,表示調(diào)用一個(gè)Web Api時(shí)發(fā)生的異常。什么是Web Api呢?最常見的就是DOM元素的一系列方法,其他還有XMLHttpRequest、Fetch等等等等,這里就不一一說明了。直接看下面一個(gè)操作DOM的例子:
var node = document.querySelector('#app');
var refnode = node.nextSibling;
var newnode = document.createElement('div');
node.insertBefore(newnode, refnode);
// 報(bào)錯(cuò):Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
單從JS代碼邏輯層面來看,沒有問題。但是代碼的操作不符合DOM的規(guī)則。
DOMException構(gòu)造函數(shù)的語法如下:
// message: 可選,錯(cuò)誤描述。
// name: 可選,錯(cuò)誤名稱。常量,具體值可以在這里找到:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMException
new DOMException([message[, name]]);
DOMException有以下三個(gè)屬性:
DOMException.code:錯(cuò)誤編號。
DOMException.message:錯(cuò)誤描述。
DOMException.name:錯(cuò)誤名稱。
以上面那段錯(cuò)誤代碼為例,其拋出的DOMException各屬性的值為:
code: 8
message: "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
name: "NotFoundError"
Promise產(chǎn)生的異常
在Promise中,如果Promise被reject了,就會(huì)拋出異常:PromiseRejectionEvent。注意,下面兩種情況都會(huì)導(dǎo)致Promise被reject:
- 業(yè)務(wù)代碼本身調(diào)用了
Promise.reject。
Promise中的代碼出錯(cuò)。
PromiseRejectionEvent的構(gòu)造函數(shù)目前在瀏覽器中大多都不兼容,這里就不說了。
PromiseRejectionEvent的屬性有兩個(gè):
PromiseRejectionEvent.promise:被reject的Promise。
PromiseRejectionEvent.reason:Promise被reject的原因。會(huì)傳遞給reject。Promsie的catch中的參數(shù)。
加載資源出錯(cuò)
由于網(wǎng)絡(luò),安全等原因,網(wǎng)頁加載資源失敗,請求接口出錯(cuò)等,也是一種常見的錯(cuò)誤。
關(guān)于錯(cuò)誤的小結(jié)
一個(gè)網(wǎng)頁在運(yùn)行過程中,可能發(fā)生四種錯(cuò)誤:
- JavaScript在運(yùn)行過程,語言自身拋出的異常。
- JavaScript在運(yùn)行過程中,調(diào)用Web Api時(shí)發(fā)生異常。
- Promise中的拒絕。
- 網(wǎng)頁加載資源,調(diào)用接口時(shí)發(fā)生異常。
我認(rèn)為,對于前兩種錯(cuò)誤,我們在平時(shí)的開發(fā)過程中,不用特別去區(qū)分,可以統(tǒng)一成:【代碼出錯(cuò)】。
捕獲錯(cuò)誤
網(wǎng)頁發(fā)生錯(cuò)誤,開發(fā)者如何捕獲這些錯(cuò)誤呢 ? 常見的有以下方法。
try...catch...
try...catch…大家都不陌生了。一般用來在具體的代碼邏輯中捕獲錯(cuò)誤。
try {
throw new Error("oops");
}
catch (ex) {
console.log("error", ex.message); // error oops
}
當(dāng)try-block中的代碼發(fā)生異常時(shí),可以在catck-block中將異常接住,瀏覽器便不會(huì)拋出錯(cuò)誤。但是,這種方式并不能捕獲異步代碼中的錯(cuò)誤,如:
try {
setTimeout(function(){
throw new Error('lala');
},0);
} catch(e) {
console.log('error', e.message);
}
這個(gè)時(shí)候,瀏覽器依然會(huì)拋出錯(cuò)誤:Uncaught Error: lala。
試想以下,如果我們將所有的代碼合理的劃分,然后都用try catch包起來,是不是就可以捕獲到所有的錯(cuò)誤了呢?可以通過編譯工具來實(shí)現(xiàn)這個(gè)功能。不過,try catch是比較耗費(fèi)性能的。
window.onerror
window.onerror = function(message, source, lineno, colno, error) { ... }
函數(shù)參數(shù):
message:錯(cuò)誤信息(字符串)
source:發(fā)生錯(cuò)誤的腳本URL(字符串)
lineno:發(fā)生錯(cuò)誤的行號(數(shù)字)
colno:發(fā)生錯(cuò)誤的列號(數(shù)字)
error:Error對象(對象)
注意,如果這個(gè)函數(shù)返回true,那么將會(huì)阻止執(zhí)行瀏覽器默認(rèn)的錯(cuò)誤處理函數(shù)。
window.addEventListener('error')
window.addEventListener('error', function(event) { ... })
我們調(diào)用Object.prototype.toString.call(event),返回的是[object ErrorEvent]??梢钥吹?code>event是ErrorEvent對象的實(shí)例。ErrorEvent是事件對象在腳本發(fā)生錯(cuò)誤時(shí)產(chǎn)生,從Event繼承而來。由于是事件,自然可以拿到target屬性。ErrorEvent還包括了錯(cuò)誤發(fā)生時(shí)的信息。
- ErrorEvent.prototype.message: 字符串,包含了所發(fā)生錯(cuò)誤的描述信息。
- ErrorEvent.prototype.filename: 字符串,包含了發(fā)生錯(cuò)誤的腳本文件的文件名。
- ErrorEvent.prototype.lineno: 數(shù)字,包含了錯(cuò)誤發(fā)生時(shí)所在的行號。
- ErrorEvent.prototype.colno: 數(shù)字,包含了錯(cuò)誤發(fā)生時(shí)所在的列號。
- ErrorEvent.prototype.error: 發(fā)生錯(cuò)誤時(shí)所拋出的 Error 對象。
注意,這里的ErrorEvent.prototype.error對應(yīng)的Error對象,就是上文提到的Error,?InternalError,RangeError,EvalError,ReferenceError,SyntaxError,TypeError,URIError,DOMException中的一種。
window.addEventListener('unhandledrejection')
window.addEventListener('unhandledrejection', function (event) { ... });
在使用Promise的時(shí)候,如果沒有聲明catch代碼塊,Promise的異常會(huì)被拋出。只能通過這個(gè)方法或者window.onunhandledrejection才能捕獲到該異常。
event就是上文提到的PromiseRejectionEvent。我們只需要關(guān)注其reason就行。
window.onerror 和 window.addEventListener('error')的區(qū)別
- 首先是事件監(jiān)聽器和事件處理器的區(qū)別。監(jiān)聽器只能聲明一次,后續(xù)的聲明會(huì)覆蓋之前的聲明。而事件處理器則可以綁定多個(gè)回調(diào)函數(shù)。
- 資源( <img> 或 <script> )加載失敗時(shí),加載資源的元素會(huì)觸發(fā)一個(gè)
Event接口的error事件,并執(zhí)行該元素上的onerror()處理函數(shù)。但這些error事件不會(huì)向上冒泡到window。不過,這些error事件能被window.addEventListener('error')捕獲。也就是說,面對資源加載失敗的錯(cuò)誤,只能用window.addEventListerner('error'),window.onerror無效。
關(guān)于錯(cuò)誤捕獲的小結(jié)
我認(rèn)為,在開發(fā)的過程中,對于容易出錯(cuò)的地方,可以使用try{}catch(){}來進(jìn)行錯(cuò)誤的捕獲,做好兜底處理,避免頁面掛掉。而對于全局的錯(cuò)誤捕獲,在現(xiàn)代瀏覽器中,我傾向于只使用使用window.addEventListener('error'),window.addEventListener('unhandledrejection')就行了。如果需要考慮兼容性,需要加上window.onerror,三者同時(shí)使用,window.addEventListener('error')專門用來捕獲資源加載錯(cuò)誤。
跨域腳本錯(cuò)誤,Script Error
在進(jìn)行錯(cuò)誤捕獲的過程中,很多時(shí)候并不能拿到完整的錯(cuò)誤信息,得到的僅僅是一個(gè)"Script Error"。
產(chǎn)生原因
由于12年前這篇文章里提到的安全問題:https://blog.jeremiahgrossman...,瀏覽器們都對內(nèi)核進(jìn)行了升級:
當(dāng)加載自不同域的腳本中發(fā)生語法錯(cuò)誤時(shí),為避免信息泄露,語法錯(cuò)誤的細(xì)節(jié)將不會(huì)報(bào)告,而是使用簡單的"Script error."代替。
一般而言,頁面的JS文件都是放在CDN的,和頁面自身的URL產(chǎn)生了跨域問題,所以引起了"Script Error"。
解決辦法
服務(wù)端添加Access-Control-Allow-Origin,頁面在script標(biāo)簽中配置?。這樣,便解決了因?yàn)榭缬蚨鴰淼?code>"Script Error"問題。
能繞過Script Error么
上面介紹了"Script Error"的標(biāo)準(zhǔn)解決方案。但是,并不是所有的瀏覽器都支持,也不是所有的服務(wù)端都能及時(shí)配置Access-Control-Allow-Origin,這種情況下,還有什么方法能在全局捕獲到所有的錯(cuò)誤,并拿到詳細(xì)信息呢?
劫持原生方法
看一個(gè)例子:
const nativeAddEventListener = EventTarget.prototype.addEventListener; // 先將原生方法保存起來。
EventTarget.prototype.addEventListener = function (type, func, options) { // 重寫原生方法。
const wrappedFunc = function (...args) { // 將回調(diào)函數(shù)包裹一層try catch
try {
return func.apply(this, args);
} catch (e) {
const errorObj = {
...
error_name: e.name || '',
error_msg: e.message || '',
error_stack: e.stack || (e.error && e.error.stack),
error_native: e,
...
};
// 接下來可以將errorObj統(tǒng)一進(jìn)行處理。
}
}
return nativeAddEventListener.call(this, type, wrappedFunc, options); // 調(diào)用原生的方法,保證addEventListener正確執(zhí)行
}
我們劫持了原生的addEventListener代碼,對addEventListener代碼中的回調(diào)函數(shù)加了一層try{}catch(){},這樣,回調(diào)函數(shù)中拋出的錯(cuò)誤會(huì)被catch住,瀏覽器不會(huì)對try-catch?起來的異常進(jìn)行跨域攔截,所以我們可以拿到詳細(xì)的錯(cuò)誤信息。通過上面的操作,我們可以拿到所有監(jiān)聽事件的回調(diào)函數(shù)中的錯(cuò)誤啦。其他的場景怎么辦呢?繼續(xù)劫持原生方法。
一個(gè)前端項(xiàng)目中,除了事件監(jiān)聽,接口請求也是一個(gè)頻繁出現(xiàn)的場景。接著上面的代碼,下面我們來劫持一下Ajax。
if (!XMLHttpRequest) {
return;
}
const nativeAjaxSend = XMLHttpRequest.prototype.send; // 首先將原生的方法保存。
const nativeAjaxOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是為了拿到請求的url
const xhrInstance = this;
xhrInstance._url = url;
return nativeAjaxOpen.apply(this, [mothod, url].concat(args));
}
XMLHttpRequest.prototype.send = function (...args) { // 對于ajax請求的監(jiān)控,主要是在send方法里處理。
const oldCb = this.onreadystatechange;
const oldErrorCb = this.onerror;
const xhrInstance = this;
xhrInstance.addEventListener('error', function (e) { // 這里捕獲到的error是一個(gè)ProgressEvent。e.target 的值為 XMLHttpRequest的實(shí)例。當(dāng)網(wǎng)絡(luò)錯(cuò)誤(ajax并沒有發(fā)出去)或者發(fā)生跨域的時(shí)候,會(huì)觸發(fā)XMLHttpRequest的error, 此時(shí),e.target.status 的值為:0,e.target.statusText 的值為:''
const errorObj = {
...
error_msg: 'ajax filed',
error_stack: JSON.stringify({
status: e.target.status,
statusText: e.target.statusText
}),
error_native: e,
...
}
/*接下來可以對errorObj進(jìn)行統(tǒng)一處理*/
});
xhrInstance.addEventListener('abort', function (e) { // 主動(dòng)取消ajax的情況需要標(biāo)注,否則可能會(huì)產(chǎn)生誤報(bào)
if (e.type === 'abort') {
xhrInstance._isAbort = true;
}
});
this.onreadystatechange = function (...innerArgs) {
if (xhrInstance.readyState === 4) {
if (!xhrInstance._isAbort && xhrInstance.status !== 200) { // 請求不成功時(shí),拿到錯(cuò)誤信息
const errorObj = {
error_msg: JSON.stringify({
code: xhrInstance.status,
msg: xhrInstance.statusText,
url: xhrInstance._url
}),
error_stack: '',
error_native: xhrInstance
};
/*接下來可以對errorObj進(jìn)行統(tǒng)一處理*/
}
}
oldCb && oldCb.apply(this, innerArgs);
}
return nativeAjaxSend.apply(this, args);
}
}
我們引用框架時(shí),某些框架會(huì)用console.error的方法拋出錯(cuò)誤。我們可以劫持console.error,來捕獲錯(cuò)誤。
const nativeConsoleError = window.console.error;
window.console.error = function (...args) {
args.forEach(item => {
if (typeDetect.isError(item)) {
...
} else {
...
}
});
nativeConsoleError.apply(this, args);
}
原生的方法有很多,還比如fetch、setTimeout等。這里不一一列舉了。但是使用劫持原生方法以覆蓋所有的場景是十分困難的。
前端框架是怎么捕獲錯(cuò)誤的
我們主要來看一下React和Vue是怎么解決錯(cuò)誤捕獲問題的。
React中的錯(cuò)誤捕獲
在Reactv16以前,可以使用unstable_handleError來處理捕獲的錯(cuò)誤。Reactv16以后,使用componentDidCatch來處理捕獲的錯(cuò)誤。若需全局捕獲錯(cuò)誤,可以在最外層包裹一層組件,在componentDidCatch中捕獲錯(cuò)誤信息。具體用法參考官方文檔:https:///blog/2017/07/26/error-handling-in-react-16.html
在React中,錯(cuò)誤會(huì)被throw出來。在寫作本文的時(shí)候,我遇到一個(gè)問題,如果在加載react相關(guān)的代碼前,按照上文的方法劫持addEventListener,那么React將不會(huì)正常工作了,但是沒有任何報(bào)錯(cuò)。React有一套自己的事件系統(tǒng),會(huì)不會(huì)和這個(gè)有關(guān)呢?之前沒有研究過React源碼,粗略調(diào)試了以下,沒有發(fā)現(xiàn)問題所在。后續(xù)會(huì)仔細(xì)研究。
Vue中的錯(cuò)誤捕獲
Vue的源碼中,在關(guān)鍵函數(shù)(比如鉤子函數(shù)等)執(zhí)行的時(shí)候,都加上try{}catch(){},在cacth中處理捕獲到的錯(cuò)誤??聪旅娴脑创a。
...
// vue源碼片段
function callHook (vm, hook) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget();
var handlers = vm.$options[hook];
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i ) {
try {
handlers[i].call(vm);
} catch (e) {
handleError(e, vm, (hook " hook"));
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' hook);
}
popTarget();
}
...
function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
logError(e, null, 'config.errorHandler');
}
}
logError(err, vm, info);
}
function logError (err, vm, info) {
{
warn(("Error in " info ": \"" (err.toString()) "\""), vm);
}
/* istanbul ignore else */
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err);
} else {
throw err
}
}
Vue中提供了Vue.config.errorHandler`來處理捕獲到的錯(cuò)誤。
// err: 捕獲到的錯(cuò)誤對象。
// vm: 出錯(cuò)的VueComponent.
// info: Vue 特定的錯(cuò)誤信息,比如錯(cuò)誤所在的生命周期鉤子
Vue.config.errorHandler = function (err, vm, info) {}
如果開發(fā)者沒有配置Vue.config.errorHandler,那么捕獲到的錯(cuò)誤會(huì)以console.error的方式輸出。
上報(bào)錯(cuò)誤
捕獲到錯(cuò)誤后,如何上報(bào)呢?最常見、最簡單的方式就是通過<img>了。代碼簡單,且沒有跨域煩惱。
function logError(error){
var img = new Image();
img.onload = img.onerror = function(){
img = null;
}
img.src = `${上報(bào)地址}?${processErrorParam(error)}`;
}
當(dāng)上報(bào)數(shù)據(jù)比較多時(shí),可以使用post的方式進(jìn)行上報(bào)。
錯(cuò)誤的上報(bào)其實(shí)是一項(xiàng)復(fù)雜的工程,涉及到上報(bào)策略、上報(bào)分類等等。特別是在項(xiàng)目的業(yè)務(wù)比較復(fù)雜的時(shí)候,更應(yīng)該關(guān)注上報(bào)的質(zhì)量,避免影響到業(yè)務(wù)功能的正常運(yùn)行。使用了打包工具處理的代碼,往往還需要結(jié)合sourceMap進(jìn)行代碼定位。本文就不做介紹了。
寫在后面
要建立一套完整、可用的前端錯(cuò)誤監(jiān)控體系是一項(xiàng)復(fù)雜、浩大的工程。但是,這項(xiàng)工程往往是必備的。本文主要介紹了你可能沒關(guān)注過的Error的一些細(xì)節(jié),以及如何捕獲頁面中的錯(cuò)誤。關(guān)于劫持原生方法部分的代碼,你可以在https://github.com/CoyPan/Fec找到。
符合預(yù)期。 來源:https://www./content-4-267751.html
|