前端異常處理

前言

為什么要處理前端異常够掠,有以下幾方面的原因:

  1. 提高代碼健壯性:對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)民褂,這點(diǎn)很重要,代碼的健壯性越好疯潭,系統(tǒng)越不容易崩潰赊堪;
  2. 提升系統(tǒng)穩(wěn)定性:異常會(huì)導(dǎo)致正常流程無(wú)法進(jìn)行、頁(yè)面樣式錯(cuò)亂竖哩、崩潰甚至白屏等問(wèn)題哭廉,嚴(yán)重的會(huì)給業(yè)務(wù)造成損失;
  3. 增強(qiáng)用戶體驗(yàn):代碼的錯(cuò)誤不應(yīng)該影響頁(yè)面的正常顯示和用戶交互相叁,出錯(cuò)時(shí)我們需要使用拖底方案或者給用戶反饋遵绰;
  4. 便于定位問(wèn)題:只有知道了如何處理異常,我們才能將異常正常上報(bào)給前端監(jiān)控系統(tǒng)增淹,及時(shí)發(fā)現(xiàn)并定位問(wèn)題椿访。

本文分為以下三個(gè)部分:
第一部分:介紹 Error 對(duì)象及 Error 的類型;
第二部分:介紹捕獲異常的方式有哪些虑润,包含通用成玫、Vue和React項(xiàng)目、iframe中的捕獲以及頁(yè)面崩潰異常的獲热鳌梁剔;
第三部分:結(jié)合工作中的場(chǎng)景,總結(jié)各自對(duì)應(yīng)的異常處理方式舞蔽。

這篇文章的前兩部分我盡量都提供了對(duì)應(yīng)的示例荣病,希望這些示例對(duì)你們有用。另外渗柿,由于這篇文章是做匯總用的个盆,會(huì)比較長(zhǎng),各位可以按自己的需要去看對(duì)應(yīng)的部分朵栖。

Error 及 Error 類型

說(shuō)到異常颊亮,我們需要先從 Error 對(duì)象講起。當(dāng) JavaScript 運(yùn)行時(shí)陨溅,如果發(fā)生了錯(cuò)誤终惑,瀏覽器就會(huì)拋出 Error 的實(shí)例對(duì)象。

Error 對(duì)象

Error 是 JavaScript 中的錯(cuò)誤類门扇,它同時(shí)也是一個(gè)構(gòu)造函數(shù)雹有,可以用來(lái)創(chuàng)建一個(gè)錯(cuò)誤對(duì)象偿渡。創(chuàng)建Error 實(shí)例對(duì)象的方法如下:

  new Error([message[, fileName[,lineNumber]]]);

此外,Error 可以像函數(shù)一樣使用霸奕,如果沒(méi)有 new溜宽,它將返回一個(gè) Error 實(shí)例對(duì)象。所以质帅, 僅僅調(diào)用 Error 產(chǎn)生的結(jié)果與通過(guò) new 關(guān)鍵字構(gòu)造 Error 實(shí)例對(duì)象生成的結(jié)果相同适揉。

Error類型

參照MDN的文檔, 還有以下錯(cuò)誤類型都繼承自 Error 對(duì)象:

  • SyntaxError
  • RangeError
  • ReferenceError
  • TypeError
  • URIError
  • EvalError
  • InternalError
  • AggregateError

接下來(lái)我將按順序介紹上述錯(cuò)誤類型的含義,并盡量舉出對(duì)應(yīng)的例子煤惩。

  1. SyntaxError

SyntaxError 是代碼不符合 Javascript 語(yǔ)法規(guī)范產(chǎn)生的錯(cuò)誤嫉嘀。

  // 變量名錯(cuò)誤
  let 1name // Uncaught SyntaxError: Invalid or unexpected token

  // 缺少括號(hào)
  console.log('test' // Uncaught SyntaxError: missing ) after argument list

  // 字符串沒(méi)有加引號(hào)
  a string // Uncaught SyntaxError: Unexpected identifier
  1. RangeError

RangeError 是當(dāng)一個(gè)值不在允許的范圍或者集合中時(shí)的錯(cuò)誤。

  // 傳遞一個(gè)不合法的length值作為Array構(gòu)造器的參數(shù)創(chuàng)建數(shù)組
  new Array(-1) // Uncaught RangeError: Invalid array length

  // 傳遞錯(cuò)誤值到數(shù)值計(jì)算方法
  var number = 10
  number.toFixed(-1) // Uncaught RangeError: toFixed() digits argument must be between 0 and 100
  1. ReferenceError

ReferenceError 是引用一個(gè)不存在的變量或者給不能賦值的對(duì)象賦值時(shí)發(fā)生的錯(cuò)誤魄揉。

  // 變量名未定義
  undefinedVariable // Uncaught ReferenceError: unknowName is not defined

  // 方法名未定義
  undefinedFunction() // Uncaught ReferenceError: undefinedFunction is not defined

  // 等號(hào)左側(cè)不能賦值 //todo: 為啥
  console.log() = 1 // Uncaught SyntaxError: Invalid left-hand side in assignment

  // 等號(hào)左側(cè)不能賦值 //todo: 為啥
  if(a === 1 || b = 2) {
      console.log('a === 1 || b = 2')
  } // Invalid left-hand side in assignment

  // this對(duì)象不能手動(dòng)賦值
  this = 1 // Uncaught SyntaxError: Invalid left-hand side in assignment
  1. TypeError

TypeError 是變量或參數(shù)的類型不是預(yù)期類型時(shí)發(fā)生的錯(cuò)誤盒卸。

  // new命令的參數(shù)不是構(gòu)造函數(shù)
  new 123 // Uncaught TypeError: 123 is not a constructor

  // 使用的方法不是function
  let functionName = 'functionName'
  functionName() // Uncaught TypeError: functionName is not a function

  // undefined或null沒(méi)有對(duì)應(yīng)的屬性或方法
  undefined.value // Uncaught TypeError: Cannot read property 'value' of undefined
  1. URIError

URIError 是 URI 相關(guān)函數(shù)的參數(shù)不正確時(shí)拋出的錯(cuò)誤峰鄙。

  decodeURI('%') // Uncaught URIError: URI malformed
  1. EvalError

EvalError 表示 eval 函數(shù)沒(méi)有被正確執(zhí)行時(shí)發(fā)生的錯(cuò)誤。需要注意的是此異常不再會(huì)被JavaScript 拋出,但是 EvalError 對(duì)象仍然保持兼容性痊焊。

  // 沒(méi)有報(bào)EvalError而是對(duì)應(yīng)執(zhí)行js時(shí)的SyntaxError
  eval('a string') // Uncaught SyntaxError: Unexpected identifier

永遠(yuǎn)不要使用 eval魔策!
eval() 是一個(gè)危險(xiǎn)的函數(shù)欣除, 它使用與調(diào)用者相同的權(quán)限執(zhí)行代碼兔朦。如果你用 eval() 運(yùn)行的字符串代碼被惡意方(不懷好意的人)修改,您最終可能會(huì)在您的網(wǎng)頁(yè)/擴(kuò)展程序的權(quán)限下摇零,在用戶計(jì)算機(jī)上運(yùn)行惡意代碼推掸。
eval() 通常比其他替代方法更慢,因?yàn)樗仨氄{(diào)用 JS 解釋器驻仅,而許多其他結(jié)構(gòu)則可被現(xiàn)代 JS 引擎進(jìn)行優(yōu)化谅畅。

  1. InternalError

InternalError 表示出現(xiàn)在 JavaScript 引擎內(nèi)部的錯(cuò)誤。

示例場(chǎng)景通常為某些成分過(guò)大噪服,例如:

  • "too many switch cases"(過(guò)多case子句)毡泻;
  • "too many parentheses in regular expression"(正則表達(dá)式中括號(hào)過(guò)多);
  • "array initializer too large"(數(shù)組初始化器過(guò)大)粘优;
  • "too much recursion"(遞歸過(guò)深)仇味。
  1. AggregateError

AggregateError 是用于把多個(gè)錯(cuò)誤集合在一起。需要注意的是這是一個(gè)實(shí)驗(yàn)中的功能雹顺,尚未被所有的瀏覽器支持(下面例子中用到的 Promise.any 也是實(shí)驗(yàn)中的功能)丹墨。

  Promise.any([
    Promise.reject(new Error("some error"))
  ]) // Uncaught (in promise) AggregateError: All promises were rejected

Promise.any() 接收一個(gè) Promise 可迭代對(duì)象,只要其中的一個(gè) promise 成功嬉愧,就返回那個(gè)已經(jīng)成功的 promise贩挣。如果可迭代對(duì)象中沒(méi)有一個(gè) promise 成功(即所有的 promises 都失敗/拒絕),就返回一個(gè)失敗的 promise 和 AggregateError 類型的實(shí)例。

我們還可以基于 Error 自定義異常類型王财,或者用 throw 方法拋出任意類型的異常卵迂,但我們本文的目標(biāo)在于捕獲并處理瀏覽器拋出的異常,這里對(duì)自定義的異常和手動(dòng) throw 的異常不做過(guò)多說(shuō)明搪搏。

捕獲異常

在了解了瀏覽器會(huì)拋出哪些異常后狭握,我們現(xiàn)在來(lái)進(jìn)一步了解在代碼層面我們可以做些什么來(lái)捕獲這些異常闪金,從而協(xié)助我們提升代碼的健壯性疯溺。

通用方式

try-catch

try-catch 語(yǔ)句標(biāo)記要嘗試的語(yǔ)句塊,并指定一個(gè)出現(xiàn)異常時(shí)拋出的響應(yīng)哎垦。try 語(yǔ)句包含了由一個(gè)或者多個(gè)語(yǔ)句組成的 try 塊囱嫩,catch 子句包含 try 塊中拋出異常時(shí)要執(zhí)行的語(yǔ)句。如果在 try 塊中有任何一個(gè)語(yǔ)句(或者從 try 塊中調(diào)用的函數(shù))拋出異常漏设,控制立即轉(zhuǎn)向 catch 子句墨闲。如果在 try 塊中沒(méi)有異常拋出,會(huì)跳過(guò) catch 子句郑口。

  try {
    const person = {};
    console.log(person.info.name);
  } catch (err) {
    console.log(err);
  }

  // TypeError: Cannot read property 'name' of undefined at <anonymous>:3:27

上面的例子中鸳碧,我們?cè)噲D獲取一個(gè) undefined 對(duì)象的屬性值,這個(gè)異常被 catch 捕獲并輸出在控制臺(tái)犬性。

任何給定的異常只會(huì)被離它最近的封閉 catch 塊捕獲一次瞻离。

有時(shí)候,我們代碼中也會(huì)出現(xiàn) try-catch 嵌套的情況乒裆,如果內(nèi)層沒(méi)有 catch 事件套利,則會(huì)被外層 catch 捕獲:

  try {
    try {
      throw new Error('error');
    }
    finally {
      console.log('finally');
    }
  }
  catch (err) {
    console.log('outer', err);
  }

  // finally
  // VM1360:10 outer Error: error at <anonymous>:3:11

如果在內(nèi)層拋出新異常,這個(gè)新異常會(huì)被外層 catch 捕獲:

  try {
    try {
      throw new Error('error');
    }
    catch (err) {
      console.log('inner', err);
      throw err; // 拋出新異常鹤耍,沒(méi)有被內(nèi)層捕獲過(guò)
    }
  }
  catch (err) {
    console.log('outer', err);
  }

  // inner Error: error at <anonymous>:3:11
  // outer Error: error at <anonymous>:3:11

try-catch 適用于知道某段代碼可能出現(xiàn)問(wèn)題的情況肉迫,只能捕獲同步的運(yùn)行時(shí)錯(cuò)誤,不能捕獲語(yǔ)法錯(cuò)誤和異步錯(cuò)誤:

  1. 語(yǔ)法錯(cuò)誤:語(yǔ)法錯(cuò)誤稿黄,try-catch 沒(méi)有正確執(zhí)行喊衫。
  try {
    let 1a = 'a';
    console.log(1a);
  } catch (err) {
    console.log('catch syntax error');
  }

  // Uncaught SyntaxError: Invalid or unexpected token
  1. 異步錯(cuò)誤:因?yàn)楫惒绞录呀?jīng)放入異步事件隊(duì)列中,無(wú)法捕捉到杆怕。
  try {
    setTimeout(() => {
      console.log(a)
    }, 1000);
  } catch (err) {
    console.log('catch async error');
  }

  // Uncaught ReferenceError: a is not defined at <anonymous>:3:17

GlobalEventHandlers.onerror

從 GlobalEventHandlers.onerror 字面本身就可以看出格侯,這個(gè) onerror 用于處理全局的錯(cuò)誤。我們先來(lái)看下 MDN 上對(duì)它的解釋:

混合事件 GlobalEventHandlers 的 onerror 屬性用于處理 error 的事件财著。

  • 當(dāng) JavaScript 運(yùn)行時(shí)錯(cuò)誤(包括語(yǔ)法錯(cuò)誤)發(fā)生時(shí)联四,window 會(huì)觸發(fā)一個(gè) ErrorEvent 接口的 error 事件,并執(zhí)行 window.onerror()撑教。
  • 當(dāng)一項(xiàng)資源(圖片或 JavaScript文件)加載失敗朝墩,加載資源的元素會(huì)觸發(fā)一個(gè) Event 接口的 error 事件,并執(zhí)行該元素上的 onerror() 處理函數(shù)。這些 error 事件不會(huì)向上冒泡到 window收苏,不過(guò)(至少在 Firefox 中)能被單一的 window.addEventListener 捕獲亿卤。

從上面的文字,我們可以得出以下的結(jié)論:

  1. 代碼發(fā)生運(yùn)行時(shí)錯(cuò)誤(包括語(yǔ)法錯(cuò)誤)時(shí)鹿霸,會(huì)觸發(fā) window 的 error 事件排吴,我們可以通 window.onerror 和 window.addEventListener('error', function(event) { ... })來(lái)捕獲;
  2. 靜態(tài)資源加載失敗時(shí)懦鼠,會(huì)觸發(fā)加載資源的元素上的 onerror 事件钻哩,由于該事件不會(huì)冒泡到 winow,因此 window.onerror 是不會(huì)捕獲到靜態(tài)資源加載失敗的錯(cuò)誤的肛冶;
  3. 如果要使用全局方法捕獲靜態(tài)資源加載失敗的錯(cuò)誤街氢,可以使用 window.addEventListener。

我們還是來(lái)通過(guò)具體的例子來(lái)驗(yàn)證一下睦袖,先定義下 window.onerror 和 window.addEventListener 這兩個(gè)方法(需要寫在所有 JavaScript 腳本的前面珊肃,否則有可能捕獲不到錯(cuò)誤):

  window.onerror = function(message, source, lineno, colno, error) {
    console.log('window.onerror catch error:', message);
  }
  window.addEventListener('error', function(event) {
    console.log('window.addEventListener catch error:', event.message)
  });
  1. 語(yǔ)法錯(cuò)誤
  let 1a = 'a';
  console.log(1a);

  // window.onerror catch error: Uncaught SyntaxError: Invalid or unexpected token
  // window.addEventListener catch error: Uncaught SyntaxError: Invalid or unexpected token
  1. 靜態(tài)資源加載錯(cuò)誤

要捕獲靜態(tài)資源加載失敗的錯(cuò)誤,我們可以在靜態(tài)資源上添加 onerror 事件:

  <script src="https://misc.360buyimg.com/jdf/lib/jquery-1.6.4.000.js" onerror="console.log('script load onerror')"></script>

  // script load onerror

如果要全局捕獲靜態(tài)資源加載的錯(cuò)誤馅笙,需要給 addEventListener 方法增加第三個(gè)參數(shù)伦乔,即設(shè)置useCapture 為 ture:

  window.addEventListener('error', function(event) {
    console.log('window.addEventListener catch error:', event.message)
  }, true); 

加載一個(gè)錯(cuò)誤的JavaSctipt文件:

  <script src="https://misc.360buyimg.com/jdf/lib/jquery-1.6.4.000.js"></script>

  // window.addEventListener catch error: <script src=?"https:?/?/?misc.360buyimg.com/?jdf/?lib/?jquery-1.6.4.000.js">?</script>?
  1. 異步錯(cuò)誤
  setTimeout(() => {
    console.log(a)
  }, 1000);
  
  // window.onerror catch error: Uncaught ReferenceError: a is not defined
  // window.addEventListener catch error: Uncaught ReferenceError: a is not defined

從上面的例子可以看出,GlobalEventHandlers.onerror 適用于需要捕獲全局的異常的情況董习。另外烈和,同 try-catch 相比,window.onerror 和 window.addEventListener 可以捕獲語(yǔ)法錯(cuò)誤和異步錯(cuò)誤阱飘,element.onerror 和 window.addEventListener 可以捕獲靜態(tài)資源加載失敗的錯(cuò)誤斥杜。

盡管 window.onerror 和 window.addEventListener 可以處理異步錯(cuò)誤,但是對(duì)于 Promise 的異步錯(cuò)誤沥匈,是捕獲不到的蔗喂。

  new Promise((resolve, reject) => {
      console.log(a)
  })

  // Uncaught (in promise) ReferenceError: a is not defined

promise-catch

Promise 的錯(cuò)誤需要使用 promise-catch 來(lái)捕獲,這些錯(cuò)誤可以是代碼運(yùn)行時(shí)的錯(cuò)誤高帖,也可以是我們處理業(yè)務(wù)邏輯時(shí) reject 的錯(cuò)誤缰儿。

  1. 代碼錯(cuò)誤
  new Promise((resolve, reject) => {
      console.log(a)
  }).catch(err => {
      console.log('promise catch error:', err.message)
  })

  // promise catch error: a is not defined
  1. reject的錯(cuò)誤
  new Promise((resolve, reject) => {
      reject(new Error('error rejected!'))
  }).catch(err => {
      console.log('promise catch error:', err.message)
  })

  // promise catch error: error rejected!

promise-catch 的適用范圍很明確,就是處理 Promise 的異常散址。但是這里有例外乖阵,async/await 雖然本質(zhì)上還是 Promise 語(yǔ)法,但是可以被 try-catch 捕獲预麸。(因此我們提倡使用 async/await 來(lái)代替純 Promise瞪浸,這樣子可以更方便的被捕獲,如果你還是使用 Promise吏祸,要記得添加 catch事件对蒲,或者依賴全局捕獲錯(cuò)誤的方法。)

  function fn() {
      return new Promise((resolve, reject) => {
          console.log(a);
          resolve();
      })
  }
  async function test() {
      try {
          await fn();
      } catch (err) {
          console.log('try-catch error:', err.message);
      }
  }
  test();

  // try-catch error: a is not defined

unhandledrejection

我們開(kāi)發(fā)的時(shí)候,如果有些 Promise 異常沒(méi)有被處理蹈矮,可以使用全局的方法來(lái)捕獲砰逻,這里用到了 unhandledrejection 事件。

  window.onunhandledrejection = function(err) {
    console.log('window.onunhandledrejection catch error:', err.reason);
  }
  window.addEventListener('unhandledrejection', function(event) {
    console.log('window.addEventListener unhandledrejection catch error:', event.reason);
  });

  // window.onunhandledrejection catch error: ReferenceError: a is not defined
  // window.addEventListener unhandledrejection catch error: ReferenceError: a is not defined

我們?cè)趯懬岸隧?xiàng)目的時(shí)候一般都是使用框架的泛鸟,除了上面的通用的捕獲異常的方法蝠咆,框架本身還提供了一些方法供我們使用。

Vue 中捕獲異常

Vue 的官方文檔沒(méi)有專門的章節(jié)來(lái)介紹異常的處理北滥「詹伲總的來(lái)說(shuō),在生產(chǎn)環(huán)境有以下幾種方式(開(kāi)發(fā)環(huán)境的錯(cuò)誤通過(guò)控制臺(tái)就可以看到碑韵,這里不再鋪開(kāi)赡茸,詳見(jiàn) Vue 官網(wǎng)中的 warnHandler 及 renderError):

  • errorHandler
  • errorCaptured

errorHandler

errorHandler 在 Vue 中用于捕獲全局的錯(cuò)誤:

  Vue.config.errorHandler = function (err, vm, info) {
    console.log('vue errorHandler: ' + err);
  }

errorHandler 可以捕獲的異常包含以下方面:

  1. 組件的渲染和觀察期間未捕獲的錯(cuò)誤

需要注意的是 template 中如果引用一個(gè)不存在的變量的話是不會(huì)被 errorHandler 捕獲的缎脾,這個(gè)錯(cuò)誤需要使用 errorHandler 捕獲祝闻。

  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return {}
      }
  }
  </script>

  // 沒(méi)有捕獲到異常

稍微修改一下,在 data 中加入 currentTime 變量遗菠,但是賦值錯(cuò)誤:

  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return {
              currentTime
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: currentTime is not defined
  1. 捕獲組件生命周期鉤子里的錯(cuò)誤(版本>=2.2.0)
  <template>
      <div>{{currentTime}}</div>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      data () {
          return {
              currentTime: Date.now()
          }
      },
      mounted () {
          console.log(currentTime)
      }
  }
  </script>

  // vue errorHandler: ReferenceError: currentTime is not defined
  1. 自定義事件處理函數(shù)內(nèi)部的錯(cuò)誤(版本>=2.4.0)

我們假設(shè)子組件使用 $emit 方法觸發(fā)了 change 事件:

  <template>
      <child @change="changeHandler" />
  </template>

  <script>
  import Child from './child'
  export default {
      name: 'ErrorTest',
      components: {
          Child
      },
      methods: {
          changeHandler () {
              console.log(changedValue)
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: changedValue is not defined
  1. v-on DOM 監(jiān)聽(tīng)器內(nèi)部拋出的錯(cuò)誤(版本>=2.6.0)
  <template>
      <button v-on:click="clickHandler">click here</button>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      methods: {
          clickHandler () {
              console.log(target)
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: target is not defined
  1. 如果任何被覆蓋的鉤子或處理函數(shù)返回一個(gè) Promise 鏈 (例如 async 函數(shù))联喘,則來(lái)自其 Promise 鏈的錯(cuò)誤也會(huì)被處理。(版本>=2.6.0)
  <template>
      <button v-on:click="clickHandler">click here</button>
  </template>

  <script>
  export default {
      name: 'ErrorTest',
      methods: {
          clickHandler () {
              return new Promise(() => {
                  console.log(target)
              }) // 必須要return辙纬,否則不會(huì)被捕獲
          }
      }
  }
  </script>

  // vue errorHandler: ReferenceError: target is not defined

errorCaptured

errorCaptured 是 Vue 在 2.5.0 新增加的鉤子函數(shù)豁遭,用于捕獲來(lái)自子組件的錯(cuò)誤。現(xiàn)在贺拣,我們依然假設(shè)子組件拋出了一個(gè)錯(cuò)誤(這里依然保留上一節(jié)提到的 errorHandler 方法):

  <template>
      <child />
  </template>

  <script>
  import Child from './child'
  export default {
      name: 'ErrorTest',
      data() {
          return {
              currentTime: Date.now()
          }
      },
      components: {
          Child
      },
      errorCaptured (err, vm, info) {
          console.log('vue errorCaptured: ' + err);
      }
  }
  </script>

  // vue errorCaptured: ReferenceError: current is not defined
  // vue errorHandler: ReferenceError: current is not defined

上面的例子顯示蓖谢,errorCaptured 先于 errorHandler 捕獲了錯(cuò)誤,如果不想再次被上級(jí)捕獲譬涡,可以在鉤子函數(shù)中返回 false 闪幽。附上Vue官網(wǎng)給出的錯(cuò)誤傳播規(guī)則

  • 默認(rèn)情況下,如果全局的 config.errorHandler 被定義涡匀,所有的錯(cuò)誤仍會(huì)發(fā)送它盯腌,因此這些錯(cuò)誤仍然會(huì)向單一的分析服務(wù)的地方進(jìn)行匯報(bào)。
  • 如果一個(gè)組件的繼承或父級(jí)從屬鏈路中存在多個(gè) errorCaptured 鉤子陨瘩,則它們將會(huì)被相同的錯(cuò)誤逐個(gè)喚起腕够。
  • 如果此 errorCaptured 鉤子自身拋出了一個(gè)錯(cuò)誤,則這個(gè)新錯(cuò)誤和原本被捕獲的錯(cuò)誤都會(huì)發(fā)送給全局的 config.errorHandler舌劳。
  • 一個(gè) errorCaptured 鉤子能夠返回 false 以阻止錯(cuò)誤繼續(xù)向上傳播帚湘。本質(zhì)上是說(shuō)“這個(gè)錯(cuò)誤已經(jīng)被搞定了且應(yīng)該被忽略”。它會(huì)阻止其它任何會(huì)被這個(gè)錯(cuò)誤喚起的 errorCaptured 鉤子和全局的 config.errorHandler甚淡。

React 中捕獲異常

React官網(wǎng)中有專門的章節(jié)介紹異常的章節(jié)——錯(cuò)誤邊界大诸。

錯(cuò)誤邊界

錯(cuò)誤邊界的概念是 React 在 React 16 引入的概念,是為了解決部分 UI 的 JavaScript 錯(cuò)誤引起的應(yīng)用崩潰問(wèn)題。

錯(cuò)誤邊界是一種 React 組件底挫,這種組件可以捕獲并打印發(fā)生在其子組件樹(shù)任何位置的 JavaScript 錯(cuò)誤恒傻,并且,它會(huì)渲染出備用 UI建邓,而不是渲染那些崩潰了的子組件樹(shù)盈厘。錯(cuò)誤邊界在渲染期間、生命周期方法和整個(gè)組件樹(shù)的構(gòu)造函數(shù)中捕獲錯(cuò)誤官边。
如果一個(gè) class 組件中定義了 static getDerivedStateFromError() 或 componentDidCatch() 這兩個(gè)生命周期方法中的任意一個(gè)(或兩個(gè))時(shí)沸手,那么它就變成一個(gè)錯(cuò)誤邊界。
只有 class 組件才可以成為錯(cuò)誤邊界組件注簿。

基于上面的說(shuō)明契吉,我們的錯(cuò)誤邊界的組件可以這樣寫:

  import React from 'react'

  import Default from '/default'
  import { uploadError } from '../utils/error'

  class ErrorBoundary extends React.Component {
    constructor (props) {
      super(props)
      this.state = { hasError: false }
    }

    static getDerivedStateFromError (err) {
      // 發(fā)生錯(cuò)誤,顯示降級(jí)后的UI
      return { hasError: true }
    }

    componentDidCatch (err, info) {
      // 可以將錯(cuò)誤日志上報(bào)給服務(wù)器
      uploadError(err)
    }

    render () {
      if (this.state.hasError) {
        return <Default />
      }
      return this.props.children
    }
  }

  export default ErrorBoundary

  // 使用:
  <ErrorBoundary>
    <Child />
  </ErrorBoundary>

錯(cuò)誤邊界的工作方式類似于 JavaScript 的 catch {}诡渴,不同的地方在于錯(cuò)誤邊界只針對(duì) React 組件捐晶。錯(cuò)誤邊界無(wú)法捕獲的錯(cuò)誤有下面幾個(gè)方面,這些異常需要使用 try-catch 等捕獲:

  • 事件處理
  • 異步代碼
  • 服務(wù)端渲染
  • 它自身拋出來(lái)的錯(cuò)誤(并非它的子組件)

iframe 異常

當(dāng)我們的頁(yè)面引用了 iframe 的時(shí)候妄辩,也可以使用 onerror 方法捕獲 iframe 的異常惑灵,但這種形式僅限于你自己的頁(yè)面和 iframe 的頁(yè)面同域名的情況:

  <iframe src="./iframe.html"></iframe>
  <script>
      window.frames[0].onerror = function (message) {
          console.log('iframe error: ' + message);
          return true;
      }
  </script>

  // iframe error: Uncaught ReferenceError: a is not defined

頁(yè)面崩潰

頁(yè)面崩潰和上面提到的異常捕獲的情況是不一樣的,頁(yè)面崩潰時(shí)眼耀,JavaScript 代碼已經(jīng)不執(zhí)行了英支。但還是有辦法來(lái)監(jiān)控到頁(yè)面崩潰的,目前有兩種:一個(gè)是load 和 beforeunload 結(jié)合哮伟, 另外一個(gè)是基于 Service Worker干花。

load 和 beforeunload 事件

我們先來(lái)看下代碼:

  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'));
  }

從上面的代碼來(lái)看,這個(gè)方法其實(shí)是利用了頁(yè)面崩潰時(shí)無(wú)法觸發(fā) beforeunload 事件來(lái)實(shí)現(xiàn)的楞黄。頁(yè)面加載完成后池凄,在 sessionStorage 中存儲(chǔ) good_exit 的值為 pending。如果頁(yè)面正常關(guān)閉谅辣, 會(huì)觸發(fā) beforeunload 事件修赞,在 beforeunload 事件中,我們將 good_exit 的值重置為 true桑阶。如果頁(yè)面崩潰了柏副,刷新頁(yè)面時(shí),從 sessionStorage 中讀取到的值就是 pending 而不是 true蚣录。

用上面的方式處理有以下問(wèn)題:

  1. 由于是 sessionStorage 存儲(chǔ)的值割择,頁(yè)面崩潰后如果用戶關(guān)閉頁(yè)面或重新打開(kāi)瀏覽器,sessionStorage 中存儲(chǔ)的 good_exit 值我們是獲取不到的萎河;
  2. 如果前進(jìn)或后退荔泳,頁(yè)面會(huì)從緩存中加載蕉饼,有時(shí)候是不會(huì)觸發(fā) load 事件的。

即使存在上面的問(wèn)題玛歌,但這個(gè)方法對(duì)我們依然有借鑒意義昧港。頁(yè)面崩潰時(shí),JavaScript 不會(huì)執(zhí)了支子,DOM 也卸載了创肥,我們對(duì)頁(yè)面的渲染是無(wú)能為力的。但我們可以在用戶再次刷新頁(yè)面時(shí)捕獲到上次的崩潰信息值朋,并將崩潰上報(bào)到監(jiān)控系統(tǒng)叹侄。如果監(jiān)控系統(tǒng)收到大量的崩潰信息,就說(shuō)明我們的頁(yè)面出現(xiàn)了嚴(yán)重的問(wèn)題了昨登,這時(shí)候我們就需要想辦法復(fù)現(xiàn)或者從代碼邏輯層面找到崩潰原因了趾代。

基于 Service Worker

基于 Service Worker 的方案其實(shí)也是利用了頁(yè)面崩潰時(shí)無(wú)法觸發(fā) beforeunload 事件來(lái)實(shí)現(xiàn)的,與 load 和 beforeunload 的區(qū)別是 Service Worker 相對(duì)于驅(qū)動(dòng)應(yīng)用的主 JavaScript 線程丰辣,它運(yùn)行在其他線程中撒强,即使網(wǎng)頁(yè)崩潰了,Service Worker 一般情況下也不會(huì)崩潰糯俗。所以尿褪,我們不需要等到用戶再次刷新頁(yè)面才能獲取上次的崩潰信息了睦擂。

// 頁(yè)面 JavaScript 代碼
if (navigator.serviceWorker.controller !== null) {
  let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒發(fā)一次心跳
  let sessionId = uuid();
  let heartbeat = function () {
    navigator.serviceWorker.controller.postMessage({
      type: 'heartbeat',
      id: sessionId,
      data: {} // 附加信息得湘,如果頁(yè)面 crash,上報(bào)的附加數(shù)據(jù)顿仇,比如頁(yè)面地址等
    });
  }
  window.addEventListener("beforeunload", function() {
    navigator.serviceWorker.controller.postMessage({
      type: 'unload',
      id: sessionId
    });
  });
  setInterval(heartbeat, HEARTBEAT_INTERVAL);
  heartbeat();
}

// Service Worker
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 檢查一次
const CRASH_THRESHOLD = 15 * 1000; // 15s 超過(guò)15s沒(méi)有心跳則認(rèn)為已經(jīng) crash
const pages = {}
let timer
function checkCrash() {
  const now = Date.now()
  for (var id in pages) {
    let page = pages[id]
    if ((now - page.t) > CRASH_THRESHOLD) {
      // 上報(bào) crash
      delete pages[id]
    }
  }
  if (Object.keys(pages).length == 0) {
    clearInterval(timer)
    timer = null
  }
}

worker.addEventListener('message', (e) => {
  const data = e.data;
  if (data.type === 'heartbeat') {
    pages[data.id] = {
      t: Date.now()
    }
    if (!timer) {
      timer = setInterval(function () {
        checkCrash()
      }, CHECK_CRASH_INTERVAL)
    }
  } else if (data.type === 'unload') {
    delete pages[data.id]
  }
})

上面代碼的思路是:

  1. 網(wǎng)頁(yè)加載后淘正,通過(guò) postMessage API 每 5s 給 sw 發(fā)送一個(gè)心跳,表示自己的在線臼闻,sw 將在線的網(wǎng)頁(yè)登記下來(lái)鸿吆,更新登記時(shí)間;
  2. 網(wǎng)頁(yè)在 beforeunload 時(shí)述呐,通過(guò) postMessage API 告知自己已經(jīng)正常關(guān)閉惩淳,sw 將登記的網(wǎng)頁(yè)清除;
  3. 如果網(wǎng)頁(yè)在運(yùn)行的過(guò)程中 crash 了乓搬,sw 中的 running 狀態(tài)將不會(huì)被清除思犁,更新時(shí)間停留在奔潰前的最后一次心跳;
  4. Service Worker 每 10s 查看一遍登記中的網(wǎng)頁(yè)进肯,發(fā)現(xiàn)登記時(shí)間已經(jīng)超出了一定時(shí)間(比如 15s)即可判定該網(wǎng)頁(yè) crash 了激蹲。

同樣的,Service Worker捕獲的錯(cuò)誤對(duì)前端監(jiān)控是很有用的江掩。

總結(jié)

具體到實(shí)際工作中学辱,我們要處理的異常分為以下幾種:

  1. 語(yǔ)法錯(cuò)誤及代碼異常:對(duì)可疑區(qū)域增加 try-catch乘瓤,全局增加 window.onerror;
  2. 數(shù)據(jù)請(qǐng)求異常:使用 promise-catch 處理 Promise 異常,使用 unhandledrejection 處理未捕獲的Promise異常策泣,使用 try-catch 處理 async/await 異常;
  3. 靜態(tài)資源加載異常:在元素上添加 onerror衙傀,全局增加 window.addEventListener;
  4. 白屏:Vue 使用 errorHandler萨咕, React 使用 componentDidCatch差油,渲染備用UI;
  5. iframe異常:同域條件下使用 onerror任洞。
  6. 頁(yè)面崩潰:load 和 beforeunload 結(jié)合或者使用 Service Worker蓄喇。

原文地址:https://yolkpie.net/2021/01/28/%E5%89%8D%E7%AB%AF%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市交掏,隨后出現(xiàn)的幾起案子妆偏,更是在濱河造成了極大的恐慌,老刑警劉巖盅弛,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钱骂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挪鹏,警方通過(guò)查閱死者的電腦和手機(jī)见秽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)讨盒,“玉大人解取,你說(shuō)我怎么就攤上這事》邓常” “怎么了禀苦?”我有些...
    開(kāi)封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)遂鹊。 經(jīng)常有香客問(wèn)我振乏,道長(zhǎng),這世上最難降的妖魔是什么秉扑? 我笑而不...
    開(kāi)封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任慧邮,我火速辦了婚禮,結(jié)果婚禮上舟陆,老公的妹妹穿的比我還像新娘误澳。我一直安慰自己,他們只是感情好吨娜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布脓匿。 她就那樣靜靜地躺著,像睡著了一般宦赠。 火紅的嫁衣襯著肌膚如雪陪毡。 梳的紋絲不亂的頭發(fā)上米母,一...
    開(kāi)封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音毡琉,去河邊找鬼铁瞒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛桅滋,可吹牛的內(nèi)容都是我干的慧耍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼丐谋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼芍碧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起号俐,我...
    開(kāi)封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泌豆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后吏饿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體踪危,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年猪落,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贞远。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笨忌,死狀恐怖蓝仲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜜唾,我是刑警寧澤杂曲,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站袁余,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咱揍。R本人自食惡果不足惜颖榜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煤裙。 院中可真熱鬧掩完,春花似錦、人聲如沸硼砰。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)题翰。三九已至恶阴,卻和暖如春诈胜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背冯事。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工焦匈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昵仅。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓缓熟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親摔笤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子够滑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345