捕獲代碼錯誤的正確姿式(一)

image.png

前言

不知道小夥伴們是否有這樣的體驗,本地開發項目調試時一切正常,一旦發佈到線上就會出現各類奇怪的問題,緣由也是多種多樣,環境變量不一樣,宿主環境不一樣,接口返回的數據格式不一樣,代碼邏輯問題等等。css

有些錯誤在開發和測試環節能被發現,有些錯誤要到了線上纔會被發現,此時傻乎乎的等着用戶反饋未免太過被動,因此,一個收集可以捕獲代碼錯誤的監控系統就顯得尤其重要。html

該系列文章目的並非讓你們可以立馬打造一個前端代碼監控系統,是旨在介紹瀏覽器環境下捕獲代碼錯誤的經常使用方式,並經過咱們所熟知的方式,打造一個前端收集代碼錯誤的插件,最後再簡單介紹一下收集到了報錯信息,如何快速定位錯誤。前端

常見錯誤類型

在開始咱們的課題以前,咱們首先須要瞭解一下常見的錯誤類型。vue

代碼書寫錯誤

此類錯誤是最容易排除的,咱們每每只須要藉助編輯器的自動檢測,或者 eslint tslint 等三方工具,在開發階段就能解決。node

固然,若是有同窗是用文本編輯器書寫代碼的,請在收下我膝蓋的同時,下載一個 vscode 體驗更好的代碼編寫環境。react

function foo()
cont bar = "string"

代碼執行錯誤

這個能夠說是平時開發中遇到最多的了,好比下面這個例子,相信有很多同窗在使用 vue 或者 react 時,經過接口獲取列表數據,若是後端同窗對於空數據沒有返回 [],而是返回了 null 或者 undefined,前端展現就會出現問題。jquery

也有的同窗會說,ts 大法好,接口定義香香的(但其實這並不能解決接口返回不一致的問題),並且,若遇到同事類型定義皆 any 的狀況。。。git

undefined.map((item) => console.log(item));

其餘的錯誤類型,好比使用了一個未定義的變量,但其實這種錯誤均可以在開發階段規避掉。github

console.log(helloWorld);

異步代碼 reject

不知道小夥伴們在使用 promise 的時候有沒有 catch reject 的習慣,我相信除了我大部分人都是有的(違心),那對於未主動 catchreject 咱們應該如何捕獲並處理?ajax

const promise = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("promise reject"));
    }, 1000);
  });
// 未使用 catch 捕獲錯誤
promise().then(console.log);

資源加載錯誤

資源加載錯誤也是一個使人頭禿的問題,若是隻是 image 那還好,如果重要的 jscss 資源加載錯誤。。。

<!-- 你說這個資源能加載?嗯?你有問題,小老弟 -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>

捕獲代碼錯誤

接下來就咱們說的幾種報錯類型來作捕獲,有的同窗可能會說,哎呀我平時用的是 vue react,這個這麼簡單的場景我不可能會犯錯的啦。

噓,憋說話,框架們拋出錯誤的方式也是用那些簡單的方法。

另外,下面說內容都有詳細的代碼和註釋,小夥伴們能夠直接 clone 代碼在本地進行調試,具體說明能夠看項目分支的 README.md

示例地址

git clone -b demo/catch-code-error https://github.com/jsjzh/tiny-codes.git

代碼執行錯誤

在平時寫代碼的時候,若是遇到可能會執行錯誤的邏輯,咱們通常會用 try catch,包裹,而後單獨作處理,可是對於一些意料以外的錯誤,好比上面的接口返回;還有異步執行的代碼中若發生錯誤,經過 try catch 是沒法捕獲到錯誤的,這個時候,window.onerror 就派上用場了。

咱們能夠這麼理解,try catch 用來捕獲能夠預料到的錯誤,window.onerror 用來捕獲預料以外的錯,也就是兜底策略。

/**
 * message {String}: 錯誤信息
 * fileName {String}: 發生錯誤的文件
 * col {Number}: 錯誤代碼的行
 * row {Number}: 錯誤代碼的列
 * error {Error}: Error 對象
 */
window.onerror = (message, fileName, col, row, error) => {
  console.log("message: ", message);
  console.log("fileName: ", fileName);
  console.log("col: ", col);
  console.log("row: ", row);
  console.log("error.name: ", error.name);
  console.log("error.message: ", error.message);
  console.log("error: ", error);
};

主動 try catch

下面的代碼咱們主動 try catch 了錯誤,說明咱們知道 try 的邏輯裏面可能會出錯,在 catch 裏面作了處理,其實這種錯誤狀況有多是不須要上報的,由於咱們已經作了對應的錯誤處理。

ps: 若認爲仍舊須要上報錯誤,能夠在 catch 中增長一段 throw error 的邏輯,這樣就能夠將錯誤拋出,被 window.onerror 捕獲。
// 同步執行代碼,經過 try catch 能夠捕獲錯誤
try {
  console.log(helloWorld);
} catch (error) {
  // 在這裏作一些自定義處理
  // ...
  // 若作了自定義處理後,仍舊須要上報錯誤,增長 throw error
  // throw error;
}

意料以外的報錯

如下這種狀況,咱們沒有預料到會報錯,則報錯會直接被 window.onerror 捕獲。

ps: 這裏在瀏覽器環境訪問了 process 對象,這是 node 環境纔有的全局變量。
// 未使用 try catch 捕獲錯誤
// 說明咱們未預料到這段代碼可能出錯
console.log(process);

image.png

異步代碼執行錯誤

異步的邏輯代碼中如有代碼執行錯誤,咱們經過 try catch 沒法直接捕獲到,這個時候有兩種辦法,第一種是給異步執行的函數包裹 try catch,這種咱們就不舉例了,第二種方法是經過 window.onerror 來捕獲錯誤。

const promise = () =>
  new Promise(() => {
    setTimeout(() => {
      console.log(helloWorld);
    }, 1000);
  });

try {
  promise().then();
} catch (error) {
  // 沒法捕獲到錯誤 error
  console.log("can't catch error");
}

image.png

能夠看到上圖,錯誤被 window.onerror 給捕獲了。

異步代碼 reject

在說異步代碼 reject 前,咱們得有一個共識,promisecatch 沒法捕獲到代碼執行錯誤,只有經過 reject() 的方式拋出的異常,纔會被 promise.catch 捕獲到,什麼意思?看下面代碼。

const promise = () =>
  new Promise(() => {
    setTimeout(() => {
      console.log(helloWorld);
    }, 1000);
  });

promise()
  .then()
  // 沒法捕獲到 helloWorld 的錯誤
  .catch(() => {
    console.log("can't catch error");
  });

image.png

promisecatch 能夠捕獲到什麼錯誤呢?答案是經過 reject 拋出的錯誤,以下。

const promise2 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // 經過 reject 拋出錯誤
      reject(new Error("promise reject"));
    }, 1000);
  });

promise2()
  .then()
  .catch((error) => {
    // 可以捕獲到錯誤
    console.log("error.name: ", error.name);
    console.log("error.message: ", error.message);
    console.log("error: ", error);
  });

image.png

上面的內容只是爲了達成咱們之間的共識,接下來咱們來講說如何捕獲未 catchreject,經過 window.onunhandledrejection

ps: 有一點須要提醒的,對於拋出的自定義錯誤,儘可能使用 new Error(...),這樣咱們不只能夠獲取到完整的錯誤信息,還能夠獲取到文件名,出錯行以及出錯列,也就是所謂的堆棧信息。

ps2: 若是是被 promise.catch 捕獲的 reject,則不會被 window.onunhandledrejection 捕獲,若仍想將此錯誤上報,能夠經過 throw error 的方式,此時將被 window.onerror 捕獲。

window.onunhandledrejection = (promiseRejectEvent) => {
  // Error 對象,由 reject(new Error()) 生成
  const reason = promiseRejectEvent.reason;
  if (reason) {
    console.log("reason.name: ", reason.name);
    console.log("reason.message: ", reason.message);
    console.log("reason.stack: ", reason.stack);
  }
};

const promise2 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // 經過 reject 拋出錯誤
      reject(new Error("promise reject"));
    }, 1000);
  });

// 未使用 catch,錯誤將被 window.onunhandledrejection 捕獲
promise2().then();

image.png

資源加載錯誤

對於資源加載錯誤,不論是 jscssimage,咱們均可以經過 window.addEventListener("error") 來監聽捕獲錯誤。

可是對於使用 background-image: url(./error.png) 致使的資源加載錯誤,以及 new Image().src = "./error.png" 致使的資源加載錯誤,經過該方法沒法捕獲到。

ps: 如有小夥伴知道有什麼辦法能夠將這些加載錯誤捕獲到的,還勞煩告訴我,我自測成功後會更新到文章中。
window.addEventListener(
  "error",
  (sourceErrorEvent) => {
    const targetElement =
      sourceErrorEvent.target || sourceErrorEvent.srcElement;
    const url = targetElement.src || targetElement.href;

    console.log("sourceError: ", url);
  },
  true
);
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
<script src="./error.js"></script>
<link rel="stylesheet" href="./error.css" />
<img src="./error.png" />

image.png

意外的小問題

相信有了上面的三個方案,咱們已經可以捕獲大部分報錯了,可是,在調試的時候發現,當加載的 js 資源是跨域的,window.onerror 就只會收集到錯誤信息 Script error.,以下圖。

image.png

爲啥會這樣?固然就是瀏覽器的策略。。。不過咱們也有對應的解決辦法。

ps: 關於某些 html 標籤資源跨域內容能夠參見 MDN 的 crossOrigin 屬性說明。

回到問題,咱們只須要作兩件事,第一:

給跨域加載的 js 資源增長一個 crossOrigin="anonymous" 屬性

<script src="http://127.0.0.1:7002/index.js" crossorigin="anonymous"></script>

第二:

保證跨域資源的服務器設置了 Access-Control-Allow-Origin: hosts 頭,而且你的瀏覽器發送 Origin: host 在其列表內,嫌麻煩的話,服務端能夠直接設置 Access-Control-Allow-Origin: *

ps: 其實不太建議設置成 *,若是你使用了 cookie,這會致使 cookie 沒法正常工做,還須要配套設置一些其餘的頭部,才能使 cookie 生效。
固然,若是資源都是統一作了 CDN 配置的,那能夠爲靜態資源的服務器單獨配置頭部。

ps2: 你不須要主動添加 Origin 頭部,這是瀏覽器的自發行爲

# Response Header
...
Access-Control-Allow-Origin: http://127.0.0.1:7001
...

# Request Header
...
Origin: http://127.0.0.1:7001
...

image.png

如上,咱們就能夠獲取到錯誤的詳細信息了。

再小小的強調一下,以上的例子你均可以在以下示例中找到。

示例地址

git clone -b demo/catch-code-error https://github.com/jsjzh/tiny-codes.git

後語

至此,捕獲代碼錯誤的正確姿式(一)就結束了,在書寫文章的時候瞭解了許許多多平時不會關注的點,對於神奇的前端代碼監控系統也有了本身的一些心得體會。

固然,有的同窗要說了,sentry 不香嘛?爲啥要費時費力本身瞎折騰?關於這點我認爲,若是咱們只是爲了解決問題,那現成的方案拿來用天然是最好的。但不折騰的話和鹹魚有啥區別?(逃

文章後續還有兩個章節,第二章我會打造一個專門收集前端錯誤信息的插件,第三章我會對於如今很常見的代碼壓縮後如何定位問題提供一些本身不太成熟的方案。

另,文筆拙劣,方案幼稚,看官們如有任何意見或建議,歡迎在評論區留言,咱們一塊兒討論討論。

頁腳

代碼即人生,我甘之如飴。

技術不斷在變
頭腦一直在線
前端路漫漫
咱們下期見

by --- 褲襠三重奏

我在這裏 gayhub@jsjzh 歡迎你們來找我玩兒。

歡迎小夥伴們直接加我,拉你進羣一塊兒搞事情,記得備註一下你是從哪裏看到文章的。

image.png

ps: 若是圖片失效,能夠加我 wechat: kimimi_king
相關文章
相關標籤/搜索