前端錯誤收集以及統一異常處理

Harlan_Hao發表於2018-11-07

程式碼是很難真正意義的完全按照開發者的想法執行的,意外情況總是層出不窮,放任不管顯然不是一個合格的開發者該做的事情,錯誤資訊該如何進行處理、收集以及分析顯得尤為重要,這篇文章就對於這部分內容進行討論。

那對於前端同學來說,錯誤往往會阻塞程式執行,並丟擲一個錯誤,給使用者極其不好的體驗。如果我們可以提前對錯誤有所準備,將錯誤捕獲做出反應,給使用者更好的體驗。也可以通過對錯誤資訊的收集和分析,主動的去發現一些潛藏著的程式碼問題,不用等著使用者繞一大個圈子來向你提bug,你就能夠第一時間拿到各種資訊。

客戶端收集

window.onerror

window.onerror會全域性的在JavaScript執行時錯誤、語法錯誤發生時觸發。

window.onerror = (msg, url, lineNum, colNum, err) => {
  console.log(`錯誤發生的異常資訊(字串):${msg}`)
  console.log(`錯誤發生的指令碼URL(字串):${url}`)
  console.log(`錯誤發生的行號(數字):${lineNum}`)
  console.log(`錯誤發生的列號(數字):${colNum}`)
  console.log(`錯誤發生的Error物件(錯誤物件):${err}`)
};
複製程式碼

注意:這裡我們可以拿到的是被throw出來,沒有被catch過的錯誤。而不能拿到promise這樣的錯誤。

凡事不會一帆風順,很多同學再嘗試的時候,一定發現了自己只能拿到一個Script error並沒有錯誤本身的message、url等資訊,在lineNum和colNum也都是0,並不是真正錯誤發生時的錯誤資訊。

原因是瀏覽器在同源策略限制下所產生的。瀏覽器出於安全上的考慮,當頁面引用的非同域的外部指令碼中丟擲了異常,此時本頁面無許可權獲得這個異常詳情, 將輸出 Script error 的錯誤資訊。在Chrome中有這樣的安全機制,他不會將完整的跨域錯誤資訊暴露給你,只在chrome中會出現這樣的情況,在Firefox,Safari中均可以正常的拿到完整的錯誤資訊。

解決Script error

如果要解決這個問題,可以使用跨源資源共享機制( CORS )

  1. 為頁面上script標籤新增crossorigin屬性。
<!-- 增加 crossorigin 屬性後,瀏覽器將自動在請求頭中新增一個 Origin 欄位,告訴伺服器自己的來源,伺服器再判斷是否返回 -->
<script src="http://xxx.xxx.xxx.x/xxx.js" crossorigin></script>
複製程式碼
  1. 響應頭中增加 Access-Control-Allow-Origin 來支援跨域資源共享。

大家可以根據自己的需求來判斷是否需要處理這個問題,收集到這一部分不完整的錯誤資訊。

unhandledrejection

在前文中提到Promise中的錯誤並不能被try...catch和window.onerror捕獲。這時候我們就需要unhandledrejection來幫我們捕獲這部分錯誤。

window.addEventListener('unhandledrejection', (e) => {
  console.log(`Promise.reject()中的內容,告訴你發生錯誤的原因:${e.reason}`);
  console.log(`Promise物件 :${e.promise}`);
});
複製程式碼

值得一提的是unhandledrejection的相容性不是很好,下面附上一張caniuse的圖

unhandledrejection caniuse

console.error

console.error常常被視為列印的日誌,可預知的錯誤,已經被捕獲的錯誤,已經被處理過的內容。所以往往會被忽視不去處理。

下面這樣的程式碼總是很常見,做了很多事情,用一個大大的try...catch,將異常捕獲然後打一個console.error完事,可能對於異常處理這樣已經完事,捕獲住了錯誤,沒有讓程式崩潰,但如果對於錯誤收集這也是不可缺少的一部分

  try {
    // some code
  } catch (err) {
    console.error(err)
  }
複製程式碼

所以稍稍改造一下console.error,讓每一次觸發console.error的時候我們可以做一些事情,例如對錯誤收集系統做一下上報什麼的。

console.error = (func => {
  return (...args) => {
    // 在這裡就可以收集到console.error的錯誤
    // 做一些事情
    func.apply(console, args);
  }
})(console.error);
複製程式碼

addEventListener('error')

有大佬一眼指出我這一塊的不足,下來學習了一下,把這一塊內容補充上去。感謝@Dikaplio ?

在客戶端方面,一些靜態資源錯誤,圖片呀,css呀,script呀,載入失敗了。前面提到的方法都是無法捕獲的。

方法一:onerror捕獲

<script src="https://cdn.xxx.com/js/test.js"  onerror="errorHandler(this)"></script>

<link rel="stylesheet" href="https://cdn.xxx.com/styles/test.css" onerror="errorHandler(this)">
複製程式碼

這樣就可以拿到這些靜態資源的錯誤,但是呢,缺點也同樣很明顯,對程式碼的侵入型強了一些,不是一個好的辦法。

方法二: addEventListener('error')

在大多數情況下addEventListener('error')和window.onerror的效果差不多。在瀏覽器中有兩種事件機制,捕獲和冒泡,這兩個方法就分別是通過捕獲和冒泡來拿到error的。

但是對於資源的載入錯誤事件中,canBubble: false,所以理所應當的window.onerror是拿不到資源載入錯誤的,而addEventListener則可以拿到錯誤。但是在拿到錯誤以後需要簡單的區分一下是資源載入錯誤還是其他錯誤,因為該方法也能夠捕獲語法錯誤等一系列其他錯誤。

方法也很簡單,他們之間有一個很明顯的區別,其他的普通錯誤會有一個message欄位,資源載入錯誤沒有這個欄位,這樣只要讓這一段程式碼執行在所有資源之前,那就可以拿到這方面的錯誤了。

window.addEventListener('error', (errorEvent) => {
    console.log(errorEvent)
    cosnole.log(errorEvent.message)
}, true)
複製程式碼

需要注意的是這裡拿到的是一個event事件,和前面不一樣,拿到的並不是一個error物件。

服務端收集

在Node服務端的收集其實和客戶端上大同小異,只是一些方法上的區別.

uncaughtException

通過Node的全域性處理,捕獲所有未被處理的錯誤,這是最後一層關卡,兜底的操作,如果還不處理的話往往會導致程式崩潰。

process.on('uncaughtException', err => {
  //do something
});
複製程式碼

unhandledRejection

在Node中,Promise中的錯誤同樣不能被try...catch和uncaughtException捕獲。這時候我們就需要unhandledRejection來幫我們捕獲這部分錯誤。

process.on('unhandledRejection', err => {
  //do something
});
複製程式碼

console.error

console.error = (func => {
  return (...args) => {
    // 在這裡就可以收集到console.error的錯誤
    // 做一些事情
    func.apply(console, args);
  }
})(console.error);
複製程式碼

藉助框架對異常的處理(以koa為例)

對於Node端我們往往,可以藉助框架對錯誤進行捕獲,像koa就可以通過app.on error對錯誤在框架這一層進行捕獲,同樣他也是捕獲內部沒有被catch到的錯誤,像promise錯誤並不能捕獲。

app.on('error', (err, ctx) => {
  // do something
});
複製程式碼

值得一提的是,我們可以在框架內部主動的觸發這個error事件,對即使已經被我們捕獲了處理過的錯誤,也繼續拋到框架這一層來,方便做很多統一處理。

ctx.app.emit('error', err, ctx);
複製程式碼

錯誤型別的總結

  1. 同步錯誤 => 可以被1.try...catch 2.window.onerror 3.process.on('uncaughtException')捕獲。

  2. 非同步錯誤 => 例如setInterval、沒有被await的非同步函式等,是不會被try...catch捕獲的,但是會被window.onerror和process.on('uncaughtException')捕獲。

  3. Promise錯誤 => Promise.reject(new Error('some wrong'));像是這樣的promise錯誤,是不會被window.onerror和process.on('uncaughtException')捕獲的,更不會被try...catch捕獲,想要捕獲它們只能,process.on('unhandledRejection')以及window.addEventListener('unhandledrejection')

注意:在區域性被try...catch了的錯誤是不會繼續往上層丟擲了的,所以全域性處理的捕獲是肯定捕獲不到的,除非在catch到以後處理完成,將錯誤繼續向上層throw。

異常的統一處理

整體思路: 在業務層對錯誤捕獲包裝後繼續向上層丟擲,在包裝中的時候,將所有的錯誤都繼承自我們自己定義的錯誤類,在錯誤類中有很多我們自定義好的錯誤型別,在丟擲的時候只需要簡單的拋一下這個錯誤型別的例項就好,在最後中介軟體的時候我們可以catch到全部的錯誤做統一的處理。這時的錯誤是被分過類,分過級的,還有一部分可能是之前從未被捕獲的,在這就可以幹很多事了。

定義錯誤類

class SystemError extends Error {
  constructor(message) {
    super(message);
    // 錯誤型別
    // 錯誤等級
    // 錯誤資訊
    // ...
  }
  static wrapper(e) {
    const error = new this(e.message);
    // 將e上的各種東西包裝到error上
    return error;
  }
}

//可以對常見的錯誤提前定義好
createDBError(xxx) {
  const sysError = SystemError.wrapper(error);
  // 寫入錯誤資訊
  // 寫入錯誤型別
  // 寫入錯誤等級
  // ...
  return sysError;
}

//這樣在業務中拋錯的時候只需要簡單的
throw createDBError(error, { someInfo });
複製程式碼

錯誤捕獲

在業務中儘可能精確的捕獲錯誤,根據錯誤,進行定級,分類等操作,然後繼續向上層丟擲。

因為要精確的捕獲錯誤,很容易造成大量try...catch巢狀的的情況,我們要儘可能的避免這樣臃腫的程式碼

  try {
    try {
      // 運算元據庫
    } catch (err) {
      throw createDBError(error, { someInfo });
    }
    try {
      // 正常業務
    } catch (err) {
      throw createBusinessError(error, { someInfo });
    }
  } catch (err) {
    throw err
  }
複製程式碼

這時候一定是我們的程式碼有問題了,這時候我們就要想是不是可以拆分開來,不會造成這樣臃腫的局面。

中介軟體統一處理

因為前面所有的錯誤我們都只做了包裝,並且繼續上報,所以在最上層的中介軟體中,我們可以對所有的錯誤進行統一處理。

  1. 所有經過我們包裝的錯誤都來自於我們自定義的類,我們可以輕易判斷哪些錯誤是我們已知的,哪些是從未捕獲到的。
  2. 可以根據錯誤型別更友好的響應請求和展示頁面。
  3. 可以根據錯誤等級來判斷哪些錯誤只需要收集哪些錯誤需要報警。
  4. ……

總結

和各種錯誤打了一段時間交道,把自己的收穫分享出來,希望大家以後在異常處理的時候可以更得心應手。

相關文章