前端開發不得不知道的異常捕獲技巧

作爲一個前端開發人員,每次看到瀏覽器控制檯信息裏面紅通通的報錯信息是不是都很緊張...... 不要怕,下面我們就來討論一下前端的異常捕獲。

異常捕獲,相對於其他知識點可能沒那麼被重視,特別是對於前端程序員。但不得不說,這又是一個不得不面對的知識點。

爲什麼要捕獲異常

首先,我們爲什麼要進行異常捕獲和上報呢?

正所謂百密一疏,用程序員的話來說就是:天下不存在沒有 bug 的程序(不接受反駁 🤐 )。即使經過各種測試,還是會存在十分隱蔽的 bug,這種不可預見的問題只有通過完善的監控機制纔能有效的減少其帶來的損失。因此,對於最接近用戶的前端來說,爲了能遠程定位問題、增強用戶體驗,異常的捕獲和上報至關重要。

目前市面上已經有一些非常完善的前端監控系統存在,如 Fundebug、Bugsnag 等,雖然這些已經能做到幫我們實時監控生產環境的異常,但是如果我們不瞭解異常是如何產生的,又怎麼能得心應手的定位並處理問題呢?

對於 JS 而言,我們面對的僅僅只是異常,異常的出現不會直接導致 JS 引擎崩潰,最多隻是終止當前代碼的執行。下面來解釋一下這句話:

<script>
  error // 沒定義過的變量,此處會報錯
  console.log('永遠不會執行');
</script>
<script>
  console.log('我繼續執行')
</script>

異常捕獲分類

這裏我做了一個腦圖歸納一些前端異常,不一定對,只是有個大概印象。如下:

下面就針對不同異常的捕獲一一分析:

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
  1. 不能捕獲語法錯誤,我們修改一個代碼,刪掉一個單引號
try {
  let name = 'Jack;
  console.log(nam);
} catch(e) {
  console.log('捕獲到異常:',e);
}

輸出:

Uncaught SyntaxError: Invalid or unexpected token

語法錯誤SyntaxError,不管是window.error還是try...catch都沒法捕獲異常。但是不用擔心,在你寫好代碼按下保存那一刻,編譯器會幫你檢查是否有語法錯誤,如果有錯誤有會有個很明顯的紅紅的波浪線,把鼠標移上去就能看到報錯信息。因此,面對SyntaxError語法錯誤,一定要小心小心再小心

  1. 異步錯誤
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() 。

/**
* @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});
}
  1. 同步運行時錯誤
window.onerror = function(message, source, lineno, colno, error) {
    // message:錯誤信息(字符串)。
    // source:發生錯誤的腳本URL(字符串)
    // lineno:發生錯誤的行號(數字)
    // colno:發生錯誤的列號(數字)
    // error:Error對象(對象)
    console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
UndefVar;

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

  1. 語法錯誤
window.onerror = function(message, source, lineno, colno, error) {
 console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
let name = 'Jack; // 少個單引號

控制檯打印出了這樣的異常:

Uncaught SyntaxError: Invalid or unexpected token

可以看出,並沒有捕獲到異常。

  1. 異步運行時錯誤
window.onerror = function(message, source, lineno, colno, error) {
    console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
setTimeout(() ={
    UndefVar;
});

同樣看到,我們捕獲了異常:

  1. 網絡請求的異常
<script>
window.onerror = function(message, source, lineno, colno, error) {
    console.log('捕獲到異常:',{message, source, lineno, colno, error});
    return true;
}
</script>
<img src="./xxx.png">

我們發現,不論是靜態資源異常,或者接口異常,錯誤都無法捕獲到。

注意:

1.window.onerror 函數只有在返回 true 的時候,異常纔不會向上拋出(瀏覽器接收後報紅),否則即使是知道異常的發生控制檯還是會顯示 Uncaught Error: xxxxx

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

3.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 。

沒有寫catchpromise中拋出的錯誤無法被onerrortry...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"

產生原因

由於 12 年前這篇文章裏提到的安全問題:https://blog.jeremiahgrossman.com/2006/12/i-know-if-youre-logged-in-anywhere.html 當加載自不同域的腳本中發生語法錯誤時,爲避免信息泄露,語法錯誤的細節將不會報告,而是使用簡單的**"Script error."代替**。

一般而言,頁面的 JS 文件都是放在 CDN 的,和頁面自身的 URL 產生了跨域問題,所以引起了"Script Error"

解決辦法

一般情況,如果出現 Script error 這樣的錯誤,基本上可以確定是跨域問題。這時候,是不會有其他太多輔助信息的,但是解決思路無非如下:

跨源資源共享機制 ( CORS ):我們爲 script 標籤添加 crossOrigin 屬性。

<script src="http://jartto.wang/main.js" crossorigin></script>

崩潰和卡頓

卡頓也就是網頁暫時響應比較慢, JS 可能無法及時執行。但崩潰就不一樣了,網頁都崩潰了,JS 都不運行了,還有什麼辦法可以監控網頁的崩潰,並將網頁崩潰上報呢?

  1. 利用 window 對象的 load 和 beforeunload 事件實現了網頁崩潰的監控。
    不錯的文章,推薦閱讀:http://jasonjl.me/blog/2015/06/21/taking-action-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'));
  }
  1. 基於以下原因,我們可以使用 Service Worker 來實現網頁崩潰的監控:

2.1Service Worker 有自己獨立的工作線程,與網頁區分開,網頁崩潰了,Service Worker一般情況下不會崩潰

2.2Service Worker 生命週期一般要比網頁還要長,可以用來監控網頁的狀態

2.3 網頁可以通過 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;
  }
}

然後我們像使用普通組件那樣使用它:

<ErrorBoundary>  
 <MyWidget />
</ErrorBoundary>

componentDidCatch() 方法像 JS 的 catch{} 模塊一樣工作,但是對於組件,只有 class 類型的組件 (class component ) 可以成爲一個 error boundaries 。

實際上,大多數情況下我們可以在整個程序中定義一個 error boundary 組件,之後就可以一直使用它了!

需要注意的是:error boundaries並不會捕捉下面這些錯誤:

  1. 事件處理器

  2. 異步代碼

  3. 服務端的渲染代碼

  4. 在 error boundaries 區域內的錯誤

總結

  1. 可疑區域增加 try...catch

  2. 全局監控 JS 異常: window.onerror

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

  4. 全局捕獲沒有 catch 的 promise 異常:unhandledrejection

  5. iframe 異常:window.error

  6. VUE errorHandler 和 React componentDidCatch

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

  8. Script Error跨域 crossOrigin 解決

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/53-SlOTF10jiI2J9itEXcg