前端異常捕獲,技術大綱
- 可疑區域增加 try...catch
- 全局監控JS異常: window.onerror
- 全局監控靜態資源異常: window.addEventListener
- 全局捕獲沒有 catch 的 promise 異常:unhandledrejection
- iframe 異常:window.error
- VUE errorHandler 和 React componentDidCatch
- 監控網頁崩潰:window 對象的 load 和 beforeunload
- Script Error跨域 crossOrigin 解決
try...catch 的誤區
try...catch只能捕獲到同步的運行時錯誤,對於語法和異步錯誤無能為力,捕獲不到。
1.同步運行時錯誤
try {
let name = 'Jack';
console.log(nam);
} catch(e) {
console.log('捕獲到異常:',e);
}
輸出:
捕獲到異常: ReferenceError: nam is not defined
at <anonymous>:3:15
2.不能捕獲語法錯誤,我們修改一個代碼,刪掉一個單引號
try {
let name = 'Jack;
console.log(nam);
} catch(e) {
console.log('捕獲到異常:',e);
}
輸出:
Uncaught SyntaxError: Invalid or unexpected token
語法錯誤SyntaxError,不管是window.error還是try...catch都沒法捕獲異常。但是不用擔心,在你寫好代碼按下保存那一刻,編譯器會幫你檢查是否有語法錯誤,如果有錯誤有會有個很明顯的紅紅的波浪線,把鼠標移上去就能看到報錯信息。因此,面對SyntaxError語法錯誤,一定要小心小心再小心
3.異步錯誤
try {
setTimeout(() => {
undefined.map(v => v);
}, 1000)
} catch(e) {
console.log('捕獲到異常:',e);
}
輸出:
Uncaught TypeError: Cannot read property 'map' of undefined
at setTimeout (<anonymous>:3:11)
可以看到,並沒有捕獲到異常。
window.onerror 不是萬能的
當JS運行時錯誤發生時,window 會觸發一個 ErrorEvent 接口的 error 事件,並執行 window.onerror() 。
1.同步運行時錯誤
window.onerror = function(message, source, lineno, colno, error) {
// message:錯誤信息(字符串)。
// source:發生錯誤的腳本URL(字符串)
// lineno:發生錯誤的行號(數字)
// colno:發生錯誤的列號(數字)
// error:Error對象(對象)
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
UndefVar;
2.語法錯誤
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
let name = 'Jack; // 少個單引號
控制台打印出了這樣的異常:
Uncaught SyntaxError: Invalid or unexpected token
可以看出,並沒有捕獲到異常。
3.異步運行時錯誤
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
UndefVar;
});
同樣看到,我們捕獲了異常:
4.網絡請求的異常
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="./xxx.png">
我們發現,不論是靜態資源異常,或者接口異常,錯誤都無法捕獲到。
注意:
window.onerror 函數只有在返回 true 的時候,異常才不會向上拋出(瀏覽器接收后報紅),否則即使是知道異常的發生控制台還是會顯示 Uncaught Error: xxxxx
window.onerror 最好寫在所有JS腳本的前面,否則有可能捕獲不到錯誤
window.onerror無法捕獲語法錯誤
捕獲靜態資源加載錯誤
window.addEventListener
當一項資源(如圖片和腳本加載失敗),加載資源的元素會觸發一個Event接口的error事件,並執行該元素上的onerror處理函數。這些error事件不會向上冒泡到window, 不過(至少在 Chrome 中)能被單一的window.addEventListener 捕獲。
<script>
window.addEventListener('error', (error) => {
console.log('捕獲到異常:', error);
}, true)
</script>
<img src="./xxxx.png">
由於網絡請求異常不會事件冒泡,因此必須在捕獲階段將其捕捉到才行,但是這種方式雖然可以捕捉到網絡請求的異常,但是無法判斷 HTTP 的狀態是 404 還是其他比如 500 等等,所以還需要配合服務端日志才進行排查分析才可以。
注意:
不同瀏覽器下返回的 error 對象可能不同,需要注意兼容處理。 需要注意避免 window.addEventListener 重復監聽。
到此為止,我們學到了:在開發的過程中,對於容易出錯的地方,可以使用try{}catch(){}來進行錯誤的捕獲,做好兜底處理,避免頁面掛掉。而對於全局的錯誤捕獲,在現代瀏覽器中,我傾向於只使用使用window.addEventListener('error'),window.addEventListener('unhandledrejection')就行了。如果需要考慮兼容性,需要加上window.onerror,三者同時使用,window.addEventListener('error')專門用來捕獲資源加載錯誤。
Promise Catch
我們知道,在 promise 中使用 catch 可以非常方便的捕獲到異步 error 。
沒有寫catch的promise中拋出的錯誤無法被onerror或try...catch捕獲到,所以務必在promise中寫catch做異常處理。
有沒有一個全局捕獲promise的異常呢?答案是有的。 Uncaught Promise Error就能做到全局監聽,使用方式:
window.addEventListener("unhandledrejection", function(e){
// e.preventDefault(); // 阻止異常向上拋出
console.log('捕獲到異常:', e);
});
Promise.reject('promise error');
所以,正如我們上面所說,為了防止有漏掉的 promise 異常,建議在全局增加一個對 unhandledrejection 的監聽,用來全局監聽 Uncaught Promise Error。
iframe 異常
對於 iframe 的異常捕獲,我們還得借力 window.onerror:
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
下面一個簡單的例子:
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
window.frames[0].onerror = function (message, source, lineno, colno, error) {
console.log('捕獲到 iframe 異常:', {message, source, lineno, colno, error});
};
</script>
Script error
在進行錯誤捕獲的過程中,很多時候並不能拿到完整的錯誤信息,得到的僅僅是一個"Script Error"。
產生原因
當加載自不同域的腳本中發生語法錯誤時,為避免信息泄露,語法錯誤的細節將不會報告,而是使用簡單的"Script error."代替。
解決辦法
一般情況,如果出現 Script error 這樣的錯誤,基本上可以確定是跨域問題。這時候,是不會有其他太多輔助信息的,但是解決思路無非如下:
跨源資源共享機制( CORS ):我們為 script 標簽添加 crossOrigin 屬性。
<script src="http://www.jshaman.com/test.js" crossorigin></script>
崩潰和卡頓
卡頓也就是網頁暫時響應比較慢, JS可能無法及時執行。但崩潰就不一樣了,網頁都崩潰了,JS都不運行了,還有什么辦法可以監控網頁的崩潰,並將網頁崩潰上報呢?
1.利用 window 對象的 load 和 beforeunload 事件實現了網頁崩潰的監控。 不錯的文章,推薦閱讀:Logging Information on Browser Crashes。
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
2.基於以下原因,我們可以使用 Service Worker 來實現網頁崩潰的監控:
- Service Worker 有自己獨立的工作線程,與網頁區分開,網頁崩潰了,Service Worker一般情況下不會崩潰
- Service Worker 生命周期一般要比網頁還要長,可以用來監控網頁的狀態
- 網頁可以通過 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 發送消息
VUE errorHandler
在Vue中,異常可能被Vue自身給try...catch了,不會傳到window.onerror事件觸發。不過不用擔心,Vue提供了特有的異常捕獲,比如Vux2.x中我們可以這樣用:
Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 異常信息
name, // 異常名稱
script, // 異常腳本url
line, // 異常行號
column, // 異常列號
stack // 異常堆棧信息
} = err;
// vm為拋出異常的 Vue 實例
// info為 Vue 特定的錯誤信息,比如錯誤所在的生命周期鈎子
}
React 異常捕獲
在React,可以使用ErrorBoundary組件包括業務組件的方式進行異常捕獲,配合React 16.0+新出的componentDidCatch API,可以實現統一的異常捕獲和日志上報。
我們來舉一個小例子,在下面這個 componentDIdCatch(error,info) 里的類會變成一個 error boundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
componentDidCatch() 方法像JS的 catch{} 模塊一樣工作,但是對於組件,只有 class 類型的組件(class component )可以成為一個 error boundaries 。
實際上,大多數情況下我們可以在整個程序中定義一個 error boundary 組件,之后就可以一直使用它了!
需要注意的是:error boundaries並不會捕捉下面這些錯誤:
事件處理器 異步代碼 服務端的渲染代碼 在 error boundaries 區域內的錯誤
有些情況下,不想讓人發現自己的捕獲代碼,這時,可以用JShaman之類平台進行代碼混淆加密,使代碼成為不可識別的亂碼。