如何優雅處理前端異常?

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

We can complete it step by step however long the road is and it can't be completedhowever short the road is if you don't even mark your footprint.

再長的路,一步步也能走完;再短的路,不邁開雙腳也無法到達。


每日掏心話

有些人,一轉身就是一輩子。許多感情疏遠淡漠,無力挽回,只源於一念之差;生命是一種永恆的修煉,沒有盡頭,說不定在某一個拐角我們就丟失了自己!




來自:Jartto's blog | 責編:樂樂

鏈接:jartto.wang/2018/11/20/js-exception-handling/

如何優雅處理前端異常?

程序員小樂(ID:study_tech)第 770 次推文 圖片來自 Pexels


往日回顧:面試字節跳動,我被懟了....


正文


前端一直是距離用戶最近的一層,隨著產品的日益完善,我們會更加註重用戶體驗,而前端異常卻如鯁在喉,甚是煩人。

一、為什麼要處理異常?

異常是不可控的,會影響最終的呈現結果,但是我們有充分的理由去做這樣的事情。

  • 增強用戶體驗;

  • 遠程定位問題;

  • 未雨綢繆,及早發現問題;

  • 無法複線問題,尤其是移動端,機型,系統都是問題;

  • 完善的前端方案,前端監控系統;

  • 對於 JS 而言,我們面對的僅僅只是異常,異常的出現不會直接導致 JS 引擎崩潰,最多隻會使當前執行的任務終止。


二、需要處理哪些異常?

對於前端來說,我們可做的異常捕獲還真不少。總結一下,大概如下:

  • JS 語法錯誤、代碼異常

  • AJAX 請求異常

  • 靜態資源加載異常

  • Promise 異常

  • Iframe 異常

  • 跨域 Script error

  • 崩潰和卡頓

  • 下面我會針對每種具體情況來說明如何處理這些異常。


三、Try-Catch 的誤區

try-catch 只能捕獲到同步的運行時錯誤,對語法和異步錯誤卻無能為力,捕獲不到。

  • 同步運行時錯誤:

  • try {


  • let name = 'jartto';


  • console.log(nam);


  • } catch(e) {


  • console.log('捕獲到異常:',e);


  • }


  • 輸出:捕獲到異常:ReferenceError: nam is not defined


  • at <anonymous>:3:15/<anonymous>


  • 不能捕獲到具體的語法錯誤,只有一個語法錯誤提示。我們修改一下代碼,刪掉一個單引號:

  • try {


  • let name = 'jartto;


  • console.log(nam);


  • } catch(e) {



  • console.log('捕獲到異常:',e);


  • }


  • 輸出:Uncaught SyntaxError: Invalid or unexpected token


  • 不過語法錯誤在我們開發階段就可以看到,應該不會順利上到線上環境。


  • 異步錯誤

  • try {


  • setTimeout(() => {


  • undefined.map(v => v);


  • }, 1000)


  • } catch(e) {


  • console.log('捕獲到異常:',e);


  • }


  • 我們看看日誌:Uncaught TypeError: Cannot read property 'map'ofundefined


  • at setTimeout (<anonymous>:3:11)/<anonymous>


  • 並沒有捕獲到異常,這是需要我們特別注意的地方。


四、window.onerror 不是萬能的

當 JS 運行時錯誤發生時,window 會觸發一個 ErrorEvent 接口的 error 事件,並執行 window.onerror()。/**


* @param {String} message 錯誤信息
* @param {String} source 出錯文件
* @param {Number} lineno 行號
* @param {Number} colno 列號
* @param {Object} error Error對象(對象)
*/

window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}

  • 首先試試同步運行時錯誤

  • window.onerror = function(message, source, lineno, colno, error) {


  • // message:錯誤信息(字符串)。


  • // source:發生錯誤的腳本URL(字符串)


  • // lineno:發生錯誤的行號(數字)


  • // colno:發生錯誤的列號(數字)


  • // error:Error對象(對象)


  • console.log('捕獲到異常:',{message, source, lineno, colno, error});


  • }


  • Jartto;


  • 可以看到,我們捕獲到了異常:


如何優雅處理前端異常?


  • 再試試語法錯誤呢?

  • window.onerror = function(message, source, lineno, colno, error) {


  • console.log('捕獲到異常:',{message, source, lineno, colno, error});


  • }


  • let name = 'Jartto


  • 控制檯打印出了這樣的異常:Uncaught SyntaxError: Invalid or unexpected token


  • 什麼,竟然沒有捕獲到語法錯誤?

  • 懷著忐忑的心,我們最後來試試異步運行時錯誤:

  • window.onerror = function(message, source, lineno, colno, error) {


  • console.log('捕獲到異常:',{message, source, lineno, colno, error});


  • }


  • setTimeout(() => {


  • Jartto;


  • });


  • 控制檯輸出了:捕獲到異常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined


  • at setTimeout (http://127.0.0.1:8001/:36:5)}


  • 接著,我們試試網絡請求異常的情況:

  • <script>


  • window.onerror = function(message, source, lineno, colno, error) {


  • console.log('捕獲到異常:',{message, source, lineno, colno, error});


  • returntrue;


  • }



  • 我們發現,不論是靜態資源異常,或者接口異常,錯誤都無法捕獲到。補充一點:window.onerror 函數只有在返回 true 的時候,異常才不會向上拋出,否則即使是知道異常的發生控制檯還是會顯示 Uncaught Error: xxxxxwindow.onerror = function(message, source, lineno, colno, error) {


  • console.log('捕獲到異常:',{message, source, lineno, colno, error});


  • returntrue;


  • }


  • setTimeout(() => {


  • Jartto;


  • });


  • 控制檯就不會再有這樣的錯誤了:Uncaught ReferenceError: Jartto is not defined


  • at setTimeout ((index):36)


  • 需要注意:

  • onerror 最好寫在所有 JS 腳本的前面,否則有可能捕獲不到錯誤;

  • onerror 無法捕獲語法錯誤;

  • 到這裡基本就清晰了:在實際的使用過程中,onerror 主要是來捕獲預料之外的錯誤,而 try-catch 則是用來在可預見情況下監控特定的錯誤,兩者結合使用更加高效。問題又來了,捕獲不到靜態資源加載異常怎麼辦?


五、window.addEventListener

當一項資源(如圖片或腳本)加載失敗,加載資源的元素會觸發一個 Event 接口的 error 事件,並執行該元素上的onerror() 處理函數。這些 error 事件不會向上冒泡到 window ,不過(至少在 Firefox 中)能被單一的window.addEventListener 捕獲。<scritp>
window.addEventListener('error', (error) => {
console.log('捕獲到異常:', error);
}, true)


控制檯輸出:
如何優雅處理前端異常?
由於網絡請求異常不會事件冒泡,因此必須在捕獲階段將其捕捉到才行,但是這種方式雖然可以捕捉到網絡請求的異常,但是無法判斷 HTTP 的狀態是 404 還是其他比如 500 等等,所以還需要配合服務端日誌才進行排查分析才可以。需要注意:/<scritp>

  • 不同瀏覽器下返回的 error 對象可能不同,需要注意兼容處理。

  • 需要注意避免 addEventListener 重複監聽。


六、Promise Catch

在 promise 中使用 catch 可以非常方便的捕獲到異步 error ,這個很簡單。沒有寫 catch 的 Promise 中拋出的錯誤無法被 onerror 或 try-catch 捕獲到,所以我們務必要在 Promise 中不要忘記寫 catch 處理拋出的異常。解決方案:為了防止有漏掉的 Promise 異常,建議在全局增加一個對 unhandledrejection 的監聽,用來全局監聽Uncaught Promise Error。使用方式:window.addEventListener("unhandledrejection", function(e){
console.log(e);
});
我們繼續來嘗試一下:window.addEventListener("unhandledrejection", function(e){
e.preventDefault()
console.log('捕獲到異常:', e);
returntrue;
});
Promise.reject('promise error');
可以看到如下輸出:

如何優雅處理前端異常?

那如果對 Promise 不進行 catch 呢?
window.addEventListener("unhandledrejection", function(e){
e.preventDefault()
console.log('捕獲到異常:', e);
returntrue;
});
newPromise((resolve, reject) => {
reject('jartto: promise error');
});
嗯,事實證明,也是會被正常捕獲到的。所以,正如我們上面所說,為了防止有漏掉的 Promise 異常,建議在全局增加一個對 unhandledrejection 的監聽,用來全局監聽 Uncaught Promise Error。補充一點:如果去掉控制檯的異常顯示,需要加上:event.preventDefault();

VUE errorHandler

Vue.config.errorHandler = (err, vm, info) => {
console.error('通過vue errorHandler捕獲的錯誤');
console.error(err);
console.error(vm);
console.error(info);
}
八、React 異常捕獲 React 16 提供了一個內置函數 componentDidCatch,使用它可以非常簡單的獲取到 react 下的錯誤信息componentDidCatch(error, info) {
console.log(error, info);
}
除此之外,我們可以瞭解一下:error boundary UI 的某部分引起的 JS 錯誤不應該破壞整個程序,為了幫 React 的使用者解決這個問題,React 16 介紹了一種關於錯誤邊界(error boundary)的新觀念。需要注意的是:error boundaries 並不會捕捉下面這些錯誤。

  • 事件處理器

  • 異步代碼

  • 服務端的渲染代碼

  • 在 error boundaries 區域內的錯誤

  • 我們來舉一個小例子,在下面這個 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

    Something went wrong.

    ;

  • }


  • returnthis.props.children;


  • }


  • }


  • 然後我們像使用普通組件那樣使用它:<errorboundary>


  • <mywidget>



  • componentDidCatch() 方法像 JS 的 catch{} 模塊一樣工作,但是對於組件,只有 class 類型的組件(class component )可以成為一個 error boundaries 。實際上,大多數情況下我們可以在整個程序中定義一個 error boundary 組件,之後就可以一直使用它了!


九、iframe 異常

對於 iframe 的異常捕獲,我們還得借力 window.onerror:window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
一個簡單的例子可能如下:<iframe>

十、Script error

一般情況,如果出現 Script error 這樣的錯誤,基本上可以確定是出現了跨域問題。這時候,是不會有其他太多輔助信息的,但是解決思路無非如下:跨源資源共享機制( CORS ):我們為/>
或者動態去添加 js 腳本:const/>script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
特別注意,服務器端需要設置:Access-Control-Allow-Origin此外,我們也可以試試這個-解決 Script Error 的另類思路:const originAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options) {
const wrappedListener = function (...args) {
try {
return listener.apply(this, args);
}
catch (err) {
throw err;
}
}
return originAddEventListener.call(this, type, wrappedListener, options);
}
簡單解釋一下:改寫了 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 = newError(`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 可能無法及時執行。但崩潰就不一樣了,網頁都崩潰了,JS 都不運行了,還有什麼辦法可以監控網頁的崩潰,並將網頁崩潰上報呢?崩潰和卡頓也是不可忽視的,也許會導致你的用戶流失。

  • 利用 window 對象的 load 和 beforeunload 事件實現了網頁崩潰的監控。不錯的文章,推薦閱讀:Logging Information on Browser Crashes。

  • window.addEventListener('load', function () {


  • sessionStorage.setItem('good_exit', 'pending');


  • setInterval(function () {


  • sessionStorage.setItem('time_before_crash', newDate().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'));


  • }


  • 基於以下原因,我們可以使用 Service Worker 來實現網頁崩潰的監控:

  • Service Worker 有自己獨立的工作線程,與網頁區分開,網頁崩潰了,Service Worker 一般情況下不會崩潰;Service Worker 生命週期一般要比網頁還要長,可以用來監控網頁的狀態;網頁可以通過 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 發送消息。


十二、錯誤上報

1.通過 Ajax 發送數據 因為 Ajax 請求本身也有可能會發生異常,而且有可能會引發跨域問題,一般情況下更推薦使用動態創建 img 標籤的形式進行上報。2.動態創建 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) // 上報錯誤信息
}
}
採集率應該通過實際情況來設定,隨機數,或者某些用戶特徵都是不錯的選擇。

十三、總結

回到我們開頭提出的那個問題,如何優雅的處理異常呢?

  • 可疑區域增加 Try-Catch

  • 全局監控 JS 異常 window.onerror

  • 全局監控靜態資源異常 window.addEventListener

  • 捕獲沒有 Catch 的 Promise 異常:unhandledrejection

  • VUE errorHandler 和 React componentDidCatch

  • 監控網頁崩潰:window 對象的 load 和 beforeunload

  • 跨域 crossOrigin 解決

  • 其實很簡單,正如上文所說:採用組合方案,分類型的去捕獲異常,這樣基本 80%-90% 的問題都化於無形。


十四、參考


  • Logging Information on Browser Crashes asonjl.me/blog/2015/06/21/taking-action-on-browser-crashes/

  • 前端代碼異常監控實戰 github.com/happylindz/blog/issues/5

  • Error Boundaries blog.csdn.net/a986597353/article/details/78469979

  • 前端監控知識點 github.com/RicardoCao-Biker/Front-End-Monitoring/blob/master/BasicKnowledge.md

  • Capture and report JavaScript errors with window.onerror blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror


如何優雅處理前端異常?

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


猜你還想看


阿里、騰訊、百度、華為、京東最新面試題彙集

Spring 體系常用項目一覽

性能優化:要2個月才跑完的程序我是如何優化到到4小時的?

Git 如何優雅地回退代碼


關注訂閱號「程序員小樂」,收看更多精彩內容
嘿,你在看嗎?



分享到:


相關文章: