前端錯誤收集以及統一異常處理
程式碼是很難真正意義的完全按照開發者的想法執行的,意外情況總是層出不窮,放任不管顯然不是一個合格的開發者該做的事情,錯誤資訊該如何進行處理、收集以及分析顯得尤為重要,這篇文章就對於這部分內容進行討論。
那對於前端同學來說,錯誤往往會阻塞程式執行,並丟擲一個錯誤,給使用者極其不好的體驗。如果我們可以提前對錯誤有所準備,將錯誤捕獲做出反應,給使用者更好的體驗。也可以通過對錯誤資訊的收集和分析,主動的去發現一些潛藏著的程式碼問題,不用等著使用者繞一大個圈子來向你提bug,你就能夠第一時間拿到各種資訊。
客戶端收集
window.onerror
window.onerror會全域性的在JavaScript執行時錯誤、語法錯誤發生時觸發。
window.onerror = (msg, url, lineNum, colNum, err) => {
console.log(`錯誤發生的異常資訊(字串):${msg}`)
console.log(`錯誤發生的指令碼URL(字串):${url}`)
console.log(`錯誤發生的行號(數字):${lineNum}`)
console.log(`錯誤發生的列號(數字):${colNum}`)
console.log(`錯誤發生的Error物件(錯誤物件):${err}`)
};
複製程式碼
注意:這裡我們可以拿到的是被throw出來,沒有被catch過的錯誤。而不能拿到promise這樣的錯誤。
凡事不會一帆風順,很多同學再嘗試的時候,一定發現了自己只能拿到一個Script error並沒有錯誤本身的message、url等資訊,在lineNum和colNum也都是0,並不是真正錯誤發生時的錯誤資訊。
原因是瀏覽器在同源策略限制下所產生的。瀏覽器出於安全上的考慮,當頁面引用的非同域的外部指令碼中丟擲了異常,此時本頁面無許可權獲得這個異常詳情, 將輸出 Script error 的錯誤資訊。在Chrome中有這樣的安全機制,他不會將完整的跨域錯誤資訊暴露給你,只在chrome中會出現這樣的情況,在Firefox,Safari中均可以正常的拿到完整的錯誤資訊。
解決Script error
如果要解決這個問題,可以使用跨源資源共享機制( CORS )
- 為頁面上script標籤新增crossorigin屬性。
<!-- 增加 crossorigin 屬性後,瀏覽器將自動在請求頭中新增一個 Origin 欄位,告訴伺服器自己的來源,伺服器再判斷是否返回 -->
<script src="http://xxx.xxx.xxx.x/xxx.js" crossorigin></script>
複製程式碼
- 響應頭中增加 Access-Control-Allow-Origin 來支援跨域資源共享。
大家可以根據自己的需求來判斷是否需要處理這個問題,收集到這一部分不完整的錯誤資訊。
unhandledrejection
在前文中提到Promise中的錯誤並不能被try...catch和window.onerror捕獲。這時候我們就需要unhandledrejection來幫我們捕獲這部分錯誤。
window.addEventListener('unhandledrejection', (e) => {
console.log(`Promise.reject()中的內容,告訴你發生錯誤的原因:${e.reason}`);
console.log(`Promise物件 :${e.promise}`);
});
複製程式碼
console.error
console.error常常被視為列印的日誌,可預知的錯誤,已經被捕獲的錯誤,已經被處理過的內容。所以往往會被忽視不去處理。
下面這樣的程式碼總是很常見,做了很多事情,用一個大大的try...catch,將異常捕獲然後打一個console.error完事,可能對於異常處理這樣已經完事,捕獲住了錯誤,沒有讓程式崩潰,但如果對於錯誤收集這也是不可缺少的一部分
try {
// some code
} catch (err) {
console.error(err)
}
複製程式碼
所以稍稍改造一下console.error,讓每一次觸發console.error的時候我們可以做一些事情,例如對錯誤收集系統做一下上報什麼的。
console.error = (func => {
return str => {
// 在這裡就可以收集到console.error的錯誤
// 做一些事情
func.call(console, str);
}
})(console.error);
複製程式碼
服務端收集
在Node服務端的收集其實和客戶端上大同小異,只是一些方法上的區別.
uncaughtException
通過Node的全域性處理,捕獲所有未被處理的錯誤,這是最後一層關卡,兜底的操作,如果還不處理的話往往會導致程式崩潰。
process.on('uncaughtException', err => {
//do something
});
複製程式碼
unhandledRejection
在Node中,Promise中的錯誤同樣不能被try...catch和uncaughtException捕獲。這時候我們就需要unhandledRejection來幫我們捕獲這部分錯誤。
process.on('unhandledRejection', err => {
//do something
});
複製程式碼
console.error
console.error = (func => {
return str => {
// 在這裡就可以收集到console.error的錯誤
// 做一些事情
func.call(console, str);
}
})(console.error);
複製程式碼
藉助框架對異常的處理(以koa為例)
對於Node端我們往往,可以藉助框架對錯誤進行捕獲,像koa就可以通過app.on error對錯誤在框架這一層進行捕獲,同樣他也是捕獲內部沒有被catch到的錯誤,像promise錯誤並不能捕獲。
app.on('error', (err, ctx) => {
// do something
});
複製程式碼
值得一提的是,我們可以在框架內部主動的觸發這個error事件,對即使已經被我們捕獲了處理過的錯誤,也繼續拋到框架這一層來,方便做很多統一處理。
ctx.app.emit('error', err, ctx);
複製程式碼
錯誤型別的總結
-
同步錯誤 => 可以被1.try...catch 2.window.onerror 3.process.on('uncaughtException')捕獲。
-
非同步錯誤 => 例如setInterval、沒有被await的非同步函式等,是不會被try...catch捕獲的,但是會被window.onerror和process.on('uncaughtException')捕獲。
-
Promise錯誤 => Promise.reject(new Error('some wrong'));像是這樣的promise錯誤,是不會被window.onerror和process.on('uncaughtException')捕獲的,更不會被try...catch捕獲,想要捕獲它們只能,process.on('unhandledRejection')以及window.addEventListener('unhandledrejection')
注意:在區域性被try...catch了的錯誤是不會繼續往上層丟擲了的,所以全域性處理的捕獲是肯定捕獲不到的,除非在catch到以後處理完成,將錯誤繼續向上層throw。
異常的統一處理
整體思路: 在業務層對錯誤捕獲包裝後繼續向上層丟擲,在包裝中的時候,將所有的錯誤都繼承自我們自己定義的錯誤類,在錯誤類中有很多我們自定義好的錯誤型別,在丟擲的時候只需要簡單的拋一下這個錯誤型別的例項就好,在最後中介軟體的時候我們可以catch到全部的錯誤做統一的處理。這時的錯誤是被分過類,分過級的,還有一部分可能是之前從未被捕獲的,在這就可以幹很多事了。
定義錯誤類
class SystemError extends Error {
constructor(message) {
super(message);
// 錯誤型別
// 錯誤等級
// 錯誤資訊
// ...
}
static wrapper(e) {
const error = new this(e.message);
// 將e上的各種東西包裝到error上
return error;
}
}
//可以對常見的錯誤提前定義好
createDBError(xxx) {
const sysError = SystemError.wrapper(error);
// 寫入錯誤資訊
// 寫入錯誤型別
// 寫入錯誤等級
// ...
return sysError;
}
//這樣在業務中拋錯的時候只需要簡單的
throw createDBError(error, { someInfo });
複製程式碼
錯誤捕獲
在業務中儘可能精確的捕獲錯誤,根據錯誤,進行定級,分類等操作,然後繼續向上層丟擲。
因為要精確的捕獲錯誤,很容易造成大量try...catch巢狀的的情況,我們要儘可能的避免這樣臃腫的程式碼
try {
try {
// 操作資料庫
} catch (err) {
throw createDBError(error, { someInfo });
}
try {
// 正常業務
} catch (err) {
throw createBusinessError(error, { someInfo });
}
} catch (err) {
throw err
}
複製程式碼
這時候一定是我們的程式碼有問題了,這時候我們就要想是不是可以拆分開來,不會造成這樣臃腫的局面。
中介軟體統一處理
因為前面所有的錯誤我們都只做了包裝,並且繼續上報,所以在最上層的中介軟體中,我們可以對所有的錯誤進行統一處理。
- 所有經過我們包裝的錯誤都來自於我們自定義的類,我們可以輕易判斷哪些錯誤是我們已知的,哪些是從未捕獲到的。
- 可以根據錯誤型別更友好的響應請求和展示頁面。
- 可以根據錯誤等級來判斷哪些錯誤只需要收集哪些錯誤需要報警。
- ……
總結
和各種錯誤打了一段時間交道,把自己的收穫分享出來,希望大家以後在異常處理的時候可以更得心應手。