為什么要進行異常處理?
很多異常是不可控的,比如資源加載異常,ajax請求異常等,會影響最終的呈現效果,做好異常處理,有大致以下幾點好處:
- 1.增強用戶體驗;
- 2.快速定位問題原因,及時發現問題。特別是移動端,機型、系統等不一樣,有了異常處理並上報,定位快;
- 3.完善前端監控系統方案。
需要處理哪些異常?
- JS語法錯誤、代碼異常
- ajax請求異常
- 靜態資源加載異常
- promise異常
- iframe異常
- 跨域 script error
- 崩潰和卡頓
異常處理的方式
try-catch
try-catch只能捕獲到同步運行時錯誤,無法捕獲語法錯誤和異步錯誤。
示例:運行時錯誤(能捕獲)
try { error; } catch(e) { console.log('捕獲到錯誤了'); console.log(e); }
示例:語法錯誤(不能捕獲)
try { var error = 'err; // 少一個單引號 } catch(e) { console.log('捕獲不到錯誤了'); console.log(e); }
上面紅色標記的錯誤大致意思為:無效或者意外的標記。但是這種語法錯誤會直接拋出來,使后面的程序代碼無法運行,直接崩潰,一般在編碼的時候就能發現這類錯誤。
示例:異步錯誤(不能捕獲)
try { setTimeout(function() { error; // 異步錯誤,沒有定義 }) } catch(e) { console.log('捕獲不到錯誤了'); console.log(e); }
window.onerror
當JS運行發生錯誤時,window會觸發一個errorEvent接口的error事件並執行window.onerror()。
window.onerror比try-catch強一些,在try-catch的基礎上,它可以捕獲異步錯誤。
/** * @param {String} message 錯誤信息 * @param {String} resource 出錯文件 * @param {Number} row 行號 * @param {Number} col 列號 * @param {Object} error 錯誤詳細信息error對象 * */ window.onerror = function(message,resource,row,col,error) { console.log('捕獲到錯誤信息'); console.log({message,resource,row,col,error}); return true; }
示例:異步錯誤(能捕獲)
setTimeout(function() { error; // 異步錯誤,沒有定義 })
注意:
1.window.onerror也是不能捕獲語法錯誤的;
2.window.onerror也不能捕獲網絡請求異常情況,如靜態資源異常、接口異常等都是不行的;
3.特別注意的是,window.onerror函數在返回true的時候,異常才不會向上拋出,否則,控制台還是會顯示Uncaught Error: xxxxx
如下示例,我們讓window.onerror函數沒有返回true.
/** * @param {String} message 錯誤信息 * @param {String} resource 出錯文件 * @param {Number} row 行號 * @param {Number} col 列號 * @param {Object} error 錯誤詳細信息error對象 * */ window.onerror = function(message,resource,row,col,error) { console.log('捕獲到錯誤信息'); console.log({message,resource,row,col,error}); } setTimeout(function() { error; // 異步錯誤,沒有定義 })
小結:從上面兩種捕獲錯誤的方式來看,window.onerror()函數主要用來捕獲意料之外的錯誤,而try-catch主要是捕獲可預見情況下的特定錯誤。
window.addEventListener
window.onerror函數不能捕獲靜態資源加載失敗的異常情況,當資源(圖片或腳本)加載失敗,加載資源的元素會觸發一個Event接口的error事件,並執行該元素上的onerror()處理函數,這些error事件不會向上冒泡到window上,但是可以被window.addEventListener捕獲。
<script type="text/javascript"> window.addEventListener('error',function(error){ console.log('捕獲到異常錯誤了'); console.log(error); },true) </script> <img src="./images/error.jpg"/>
由於網絡請求異常不會事件冒泡,因此需要在捕獲階段將其捕捉到才行,這種方式雖然可以捕獲到網絡請求異常,但是無法判斷HTTP的狀態碼是404還是其他的如500等,所有還需要配合服務端日志進行排查分析才可以。
unhandledrejection監聽UnCaught Promise Error
在很多時候我們使用Promise的時候忘記了寫catch,那么可以在全局增加一個unhandledrejection的監聽,用來全局監聽UnCaught Promise Error,使用方式如下:
window.addEventListener("unhandledrejection", function(e){ e.preventDefault(); // 去掉控制台的異常顯示 console.log('捕獲到異常:', e); return true; }); Promise.reject('promise error');
VUE errorHandler
Vue.config.errorHandler = (err, vm, info) => { console.error('通過vue errorHandler捕獲的錯誤'); console.error(err); console.error(vm); console.error(info); }
如果在組件渲染時出現運行錯誤,錯誤將會被傳遞至全局 Vue.config.errorHandler
配置函數 (如果已設置)。利用這個鈎子函數來配合錯誤跟蹤服務是個不錯的主意。比如 Sentry,它為 Vue 提供了官方集成。
iframe異常
iframe的異常捕獲需要借助window.onerror:
<iframe src="./a.html" frameborder="0"></iframe> <script type="text/javascript"> /** * @param {String} message 錯誤信息 * @param {String} resource 出錯文件 * @param {Number} row 行號 * @param {Number} col 列號 * @param {Object} error 錯誤詳細信息error對象 * */ window.frames[0].onerror = function(message,resource,row,col,error) { console.log('捕獲到錯誤信息'); console.log({message,resource,row,col,error}); return true; } </script>
我在a.html中添加了一句JS代碼如下:
<script type="text/javascript"> var a= '1; // 缺少引號 </script>
那么在父窗口捕獲到的錯誤是:
Script error
出現Script error的情況,基本上是跨域問題。例如我們的工程中的靜態資源使用CDN,我們引入的CDN方式可能是有不同的域名,如果沒有進行額外的配置,就會出現Script error。
解決思路:跨源資源共享機制(CORS),為 script 標簽添加 crossOrigin 屬性:
<script src="http://localhost:8081/index.js" crossorigin></script>
或者動態去添加 js 腳本:
const script = document.createElement('script'); script.crossOrigin = 'anonymous'; script.src = url; document.body.appendChild(script);
特別注意,服務器端需要設置:Access-Control-Allow-Origin
解決 Script Error 的另類思路:改寫 EventTarget 的 addEventListener 方法;對傳入的 listener 進行包裝,返回包裝過的 listener,對其執行進行 try-catch;瀏覽器不會對 try-catch 起來的異常進行跨域攔截,所以 catch 到的時候,是有堆棧信息的;重新 throw 出來異常的時候,執行的是同域代碼,所以 window.onerror 捕獲的時候不會丟失堆棧信息;利用包裝 addEventListener,我們還可以達到「擴展堆棧」的效果:
(() => { const originAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function (type, listener, options) { // 捕獲添加事件時的堆棧 const addStack = new Error(`Event (${type})`).stack; const wrappedListener = function (...args) { try { return listener.apply(this, args); } catch (err) { // 異常發生時,擴展堆棧 err.stack += '\n' + addStack; throw err; } } return originAddEventListener.call(this, type, wrappedListener, options); } })()
崩潰和卡頓
卡頓也就是網頁暫時響應比較慢, JS 可能無法及時執行。但是網頁崩潰可以利用 window 對象的 load 和 beforeunload 事件實現了網頁崩潰的監控。
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')); }
錯誤上報
通過 Ajax 發送數據 因為 Ajax 請求本身也有可能會發生異常,而且有可能會引發跨域問題,一般情況下更推薦使用動態創建 img 標簽的形式進行上報。
動態創建 img 標簽的形式:
function report(error) { let reportUrl = 'http://jartto.wang/report'; new Image().src = `${reportUrl}?logs=${error}`; }
收集異常信息量太多,怎么辦?實際中,我們不得不考慮這樣一種情況:如果你的網站訪問量很大,那么一個必然的錯誤發送的信息就有很多條,這時候,我們需要設置采集率,從而減緩服務器的壓力:
Reporter.send = function(data) { // 只采集 30% if(Math.random() < 0.3) { send(data) // 上報錯誤信息 } }
采集率應該通過實際情況來設定,隨機數,或者某些用戶特征都是不錯的選擇。
也可以通過Sentry來實現前端的異常監控及上報。
總結
- 可疑區域增加 Try-Catch
- 全局監控 JS 異常 window.onerror
- 全局監控靜態資源異常 window.addEventListener
- 捕獲沒有 Catch 的 Promise 異常:unhandledrejection
- VUE errorHandler
- 監控網頁崩潰:window 對象的 load 和 beforeunload
- 跨域 crossOrigin 解決