1. 程式人生 > >前端錯誤收集以及統一異常處理

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

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

那對於前端同學來說,錯誤往往會阻塞程式執行,並丟擲一個錯誤,給使用者極其不好的體驗。如果我們可以提前對錯誤有所準備,將錯誤捕獲做出反應,給使用者更好的體驗。也可以通過對錯誤資訊的收集和分析,主動的去發現一些潛藏著的程式碼問題,不用等著使用者繞一大個圈子來向你提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}`);
});
複製程式碼

console.error

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

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

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

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

console.error = (func => {
  return str => {
    // 在這裡就可以收集到console.error的錯誤
    // 做一些事情
    func.call(console, str);
  }
})(console.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 str => {
    // 在這裡就可以收集到console.error的錯誤
    // 做一些事情
    func.call(console, str);
  }
})(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. ……

總結

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