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

代碼是很難真正意義的徹底按照開發者的想法運行的,意外狀況老是層出不窮,聽任無論顯然不是一個合格的開發者該作的事情,錯誤信息該如何進行處理、收集以及分析顯得尤其重要,這篇文章就對於這部份內容進行討論。javascript

那對於前端同窗來講,錯誤每每會阻塞程序運行,並拋出一個錯誤,給用戶極其很差的體驗。若是咱們能夠提早對錯誤有所準備,將錯誤捕獲作出反應,給用戶更好的體驗。也能夠經過對錯誤信息的收集和分析,主動的去發現一些潛藏着的代碼問題,不用等着用戶繞一大個圈子來向你提bug,你就可以第一時間拿到各類信息。css

客戶端收集

window.onerror

window.onerror會全局的在JavaScript運行時錯誤、語法錯誤發生時觸發。html

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,並非真正錯誤發生時的錯誤信息。java

緣由是瀏覽器在同源策略限制下所產生的。瀏覽器出於安全上的考慮,當頁面引用的非同域的外部腳本中拋出了異常,此時本頁面無權限得到這個異常詳情, 將輸出 Script error 的錯誤信息。在Chrome中有這樣的安全機制,他不會將完整的跨域錯誤信息暴露給你,只在chrome中會出現這樣的狀況,在Firefox,Safari中都可以正常的拿到完整的錯誤信息。chrome

解決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來幫咱們捕獲這部分錯誤。promise

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. ……

總結

和各類錯誤打了一段時間交道,把本身的收穫分享出來,但願你們之後在異常處理的時候能夠更駕輕就熟。

相關文章
相關標籤/搜索