談談前端異常捕獲

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

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

爲何要捕獲異常

首先,咱們爲何要進行異常捕獲和上報呢?vue

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

目前市面上已經有一些很是完善的前端監控系統存在,如FundebugBugsnag等,雖然這些已經能作到幫咱們實時監控生產環境的異常,可是若是咱們不瞭解異常是如何產生的,又怎麼能駕輕就熟的定位並處理問題呢?程序員

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

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

error信息

異常捕獲分類

這裏我作了一個腦圖概括一些前端異常,不必定對,只是有個大概印象。以下:跨域

前端異常分類

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

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()

/**
* @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;

能夠看到,咱們捕獲了異常:

捕獲到異常

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

沒有寫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年前這篇文章裏提到的安全問題:blog.jeremiahgrossman.com/2006/12/i-k…

當加載自不一樣域的腳本中發生語法錯誤時,爲避免信息泄露,語法錯誤的細節將不會報告,而是使用簡單的"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 對象的 loadbeforeunload 事件實現了網頁崩潰的監控。
不錯的文章,推薦閱讀: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;
  }
}

而後咱們像使用普通組件那樣使用它:

<ErrorBoundary>  
    <MyWidget />
</ErrorBoundary>

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

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

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

  • 事件處理器
  • 異步代碼
  • 服務端的渲染代碼
  • error boundaries 區域內的錯誤

總結

  1. 可疑區域增長 try...catch
  2. 全局監控JS異常: window.onerror
  3. 全局監控靜態資源異常: window.addEventListener
  4. 全局捕獲沒有 catchpromise 異常:unhandledrejection
  5. iframe 異常:window.error
  6. VUE errorHandlerReact componentDidCatch
  7. 監控網頁崩潰:window 對象的 loadbeforeunload
  8. Script Error跨域 crossOrigin 解決

參考:

前端開發中的Error以及異常捕獲

如何優雅處理前端異常?

一道不同的前端架構師最終面試題

相關文章
相關標籤/搜索