前端異常的捕獲與處理

這是第 89 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: 前端異常的捕獲與處理

按鍵沒法點擊、元素不展現、頁面白屏,這些都是咱們前端不想看到的場景。在計算機程序運行的過程當中,也老是會出現各類各樣的異常。下面就讓咱們聊一聊有哪些異常以及怎麼處理它們。javascript

1、前言

什麼是異常,異常就是預料以外的事件,每每影響了程序的正確運行。例以下面幾種場景:css

  • 頁面元素異常(例如按鈕沒法點擊、元素不展現)
  • 頁面卡頓
  • 頁面白屏

這些狀況都是極其影響用戶體驗的。對於前端來講,異常雖然不會致使計算機宕機,可是每每會致使用戶的操做被阻塞。雖然異常不可徹底杜絕,可是咱們有充分的理由去理解異常、學習處理異常。html

異常處理在程序設計中的重要性是毋庸置疑的。任何有影響力的 Web 應用程序都須要一套完善的異常處理機制,但實際上,一般只有服務端團隊會在異常處理機制上投入較大精力。雖然客戶端應用程序的異常處理也一樣重要,但真正受到重視,仍是最近幾年的事。做爲新世紀的傑出前端開發人員,咱們必須理解有哪些異常,當發生異常時咱們有哪些手段和工具能夠利用。前端

2、異常分類

從根本上來講,異常就是一個數據結構,它存了異常發生時相關信息,譬如錯誤碼、錯誤信息等。其中 message 屬性是惟一一個可以保證全部瀏覽器都支持的屬性,除此以外,IE、Firefox、Safari、Chrome 以及 Opera 都爲事件對象添加了其它相關信息。譬如 IE 添加了與 message 屬性徹底相同的 description 屬性,還添加了保存這內部錯誤數量的 number 屬性。Firefox 添加了 fileName、lineNumber 和 stack(包含堆棧屬性)。因此,在考慮瀏覽器兼容性時,最好仍是隻使用 message 屬性。vue

執行 JS 期間可能會發生的錯誤有不少類型。每種錯誤都有對應的錯誤類型,而當錯誤發生的時候就會拋出響應的錯誤對象。ECMA-262 中定義了下列 7 種錯誤類型:java

  • Error:錯誤的基類,其餘錯誤都繼承自該類型
  • EvalError:Eval 函數執行異常
  • RangeError:數組越界
  • ReferenceError:嘗試引用一個未被定義的變量時,將會拋出此異常
  • SyntaxError:語法解析不合理
  • TypeError:類型錯誤,用來表示值的類型非預期類型時發生的錯誤
  • URIError:以一種錯誤的方式使用全局 URI 處理函數而產生的錯誤

3、異常處理

ECMA-262 第 3 版中引入了 try-catch 語句,做爲 JavaScript 中處理異常的一種標準方式,基本的語法以下所示。這和 Java 中的 try-catch 語句是全完相同的。ios

try {
  // 可能會致使錯誤的代碼
} catch (error) {
  // 在錯誤發生時怎麼處理
}

若是 try 塊中的任何代碼發生了錯誤,就會當即退出代碼執行過程,而後執行 catch 塊。此時 catch 塊會接收到一個包含錯誤信息的對象,這個對象中包含的信息因瀏覽器而異,但共同的是有一個保存着錯誤信息的 message 屬性。axios

finally 子句在 try-catch 語句中是可選的,可是 finally 子句一經使用,其代碼不管如何都會執行。換句話說,try 語句塊中代碼所有正常執行,finally 子句會執行;若是由於出錯執行了 catch 語句,finally 子句照樣會執行。只要代碼中包含 finally 子句,則不管 try 或 catch 語句中包含什麼代碼——甚至是 return 語句,都不會阻止 finally 子句執行。來看下面函數的執行結果:api

function testFinally {
  try {
    return "出去玩";
  } catch (error) {
    return "看電視";
  } finally {
    return "作做業";
  }
  return "睡覺";
}

表面上調用這個函數會返回 "出去玩",由於返回 "出去玩" 的語句位於 try 語句塊中,而執行此語句又不會出錯。實際上返回 "作做業",由於最後還有 finally 子句,結果就會致使 try 塊裏的 return 語句被忽略,也就是說調用的結果只能返回 "作做業"。若是把 finally 語句拿掉,這個函數將返回 "出去玩"。所以,在使用 finally 子句以前,必定要很是清楚你想讓代碼怎麼樣。(思考一下若是 catch 塊和 finally 塊都拋出異常,catch 塊的異常是否能拋出)數組

但使人遺憾的是 ,try-catch 沒法處理異步代碼和一些其餘場景。接下來讓我具體分析幾種異常場景及其處理方案。

4、異常分析

1. JS 代碼錯誤

下面爲我司內部錯誤監控平臺一第二天常報錯的調用堆棧截圖:

錯誤仍是比較明顯的,this 指向致使的問題。onOk 使用普通函數時,函數內執行語句的 this 上下文爲 Antd.Modal 組件的實例,而 Antd.Modal 組件不存在 changeFilterType 這個方法。將 onOK 方法像 onCancel 方法同樣改爲箭頭函數,將 this 指向父組件便可。

TypeError 類型在 JavaScript 中會常常遇到,在變量中保存着意外類型時,或者在訪問不存在的方法時,都會致使這種錯誤。錯誤的緣由雖然多種多樣,但歸根結底仍是因爲在執行特定類型的操做時,變量的類型並不符合要求所致。再看幾個例子:

class People {
  constructor(name) {
    this.name = name;
  }
  sing() {}
}
const xiaoming = new People("小明");
xiaoming.dance(); // 拋出 TypeError
xiaoming.girlfriend.name; // 拋出 TypeError

代碼錯誤通常在開發和測試階段就能發現。用 try-catch 也能捕獲到:

// 代碼
try {
  xiaoming.girlfriend.name;
} catch (error) {
  console.log(xiaoming.name + "沒有女友", error);
}
// 運行結果
// 小明沒有女友 TypeError: Cannot read property 'name' of undefined

2. JS 語法錯誤

咱們修改一下代碼,咱們把英文分號改爲中文分號:

try {
  xiaoming.girlfriend.name;// 結尾是中文分號
} catch(error) {
  console.log(xiaoming.name + "沒有女友", error);
}
// 運行結果
// Uncaught SyntaxError: Invalid or unexpected token

SyntaxError 語法錯誤咱們沒法經過 try-catch 捕獲到,不過語法錯誤在咱們開發階段就能夠看到,應該不會順利上到線上環境。

不過凡事總有例外,線上仍是能收到一些語法錯誤的告警,但多半是 JSON 解析出錯和瀏覽器兼容性致使。

再看幾個例子:

JSON.parse('{name:xiaoming}');      // Uncaught SyntaxError: Unexpected token n in JSON at position 1
JSON.parse('{"name":xiaoming}');    // Uncaught SyntaxError: Unexpected token x in JSON at position 8
JSON.parse('{"name":"xiaoming"}');  // 正常
var testFunc () => { };             // 在 IE 下會拋出 SyntaxError,由於 IE 不支持箭頭函數,須要經過Babel等工具事先轉譯下

使用 JSON.parse 解析時出現異常就是一個很好的使用 try-catch 的場景:

try {
  JSON.parse(remoteData); // remoteData 爲服務端返回的數據
} catch {
  console.error("服務端數據格式返回異常,沒法解析", remoteData);
}

並非捕獲到錯誤就結束了,捕獲到錯誤後,咱們須要思考當錯誤發生時:

  • 錯誤是不是致命的,會不會致使其它連帶錯誤
  • 後續的代碼邏輯還能不能繼續執行,用戶還能不能繼續操做
  • 是否是須要將錯誤信息反饋給用戶,提示用戶如何處理該錯誤
  • 是否是須要將錯誤上報服務端

對應上面的問題這裏就會有不少解決方案了,譬如:

  1. 若是是服務器未知異常致使,能夠阻塞用戶操做,彈窗提示用戶"服務器異常,請稍後重試"。並提供給用戶一個刷新的按鈕;
try {
  return JSON.parse(remoteData);
} catch (error) {
  Modal.fail("服務器異常,請稍後重試");
  return false;
}
  1. 若是是數據異常致使,可阻塞用戶操做,彈窗提示用戶"服務器異常,請聯繫客服處理~",同時將錯誤信息上報異常服務器,開發人員經過異常堆棧和用戶埋點定位問題緣由;
try {
  return JSON.parse(remoteData);
} catch (error) {
  Modal.fail("服務器異常,請聯繫客服處理~");
  logger.error("JSON數據解析出現異常", error);
  return false;
}
  1. 若是數據解析出錯屬於預料之中的狀況,也有替代的默認值,那麼當解析出錯時直接使用默認值也能夠;
try {
  return JSON.parse(remoteData);
} catch (error) {
  console.error("服務端數據格式返回異常,使用本地緩存數據", erorr);
  return localData;
}

任何錯誤處理策略中最重要的一個部分,就是肯定錯誤是否致命。

3. 異步錯誤

try {
  setTimeout(() => {
    undefined.map(v => v);
  }, 1000)
} catch(e) {
  console.log("捕獲到異常:", e);
}
 
Uncaught TypeError: Cannot read property 'map' of undefined
  at <anonymous>:3:15

並無捕獲到異常,try-catch 對語法和異步錯誤卻無能爲力,捕獲不到,這是須要咱們特別注意的地方。

5、異常捕獲

5.1 window.onerror

JS 運行時錯誤發生時,window 會觸發一個 ErrorEvent 接口的 error 事件,並執行window.onerror()

/**
 * @param {String}  message    錯誤信息
 * @param {String}  source     出錯文件
 * @param {Number}  lineno     行號
 * @param {Number}  colno      列號
 * @param {Object}  error      Error對象(對象)
 */
window.onerror = function (message, source, lineno, colno, error) {
  console.log("捕獲到異常:", { message, source, lineno, colno, error });
};

同步錯誤能夠捕獲到,可是,請注意 window.error 沒法捕獲靜態資源異常和 JS 代碼錯誤。

5.2 靜態資源加載異常

方法一:onerror 來捕獲

<script>
  function errorHandler(error) {
    console.log("捕獲到靜態資源加載異常", error);
  }
</script>
<script src="http://cdn.xxx.com/js/test.js" onerror="errorHandler(this)"></script>
<link rel="stylesheet" href="http://cdn.xxx.com/styles/test.css" onerror="errorHandler(this)">

這樣能夠拿到靜態資源的錯誤,但缺點很明顯,代碼的侵入性太強了,每個靜態資源標籤都要加上 onerror 方法。

方法二:addEventListener("error")

<!DOCTYPE html>
<html lang="zh">
 
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>error</title>
  <script>
    window.addEventListener('error', (error) => {
      console.log('捕獲到異常:', error);
    }, true)
  </script>
</head>
 
<body>
  ![](https://itemcdn.zcycdn.com/15af41ec-e6cb-4478-8fad-1a47402f0f25.png)
</body>
 
</html>

因爲網絡請求異常不會事件冒泡,所以必須在捕獲階段將其捕捉到才行,可是這種方式雖然能夠捕捉到網絡請求的異常,可是沒法判斷 HTTP 的狀態是 404 仍是其餘好比 500 等等,因此還須要配合服務端日誌才進行排查分析才能夠。

5.3 Promise 異常

Promise 中的異常不能被 try-catch 和 window.onerror 捕獲,這時候咱們就須要監聽 unhandledrejection 來幫咱們捕獲這部分錯誤。

window.addEventListener("unhandledrejection", function (e) {
  e.preventDefault();
  console.log("捕獲到 promise 錯誤了");
  console.log("錯誤的緣由是", e.reason);
  console.log("Promise 對象是", e.promise);
  return true;
});

Promise.reject("promise error");
new Promise((resolve, reject) => {
  reject("promise error");
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw "promise error";
});

5.4 React 異常

React 處理異常的方式不一樣。雖然 try-catch 適用於許多非普通 JavaScript 應用程序,但它只適用於命令式代碼。由於 React 組件是聲明性的,因此 try-catch 不是一個可靠的選項。爲了彌補這一點,React 實現了所謂的錯誤邊界。錯誤邊界是 React 組件,它「捕獲子組件樹中的任何地方的 JavaScript 錯誤」,同時還記錄錯誤並顯示回退用戶界面。

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

  componentDidCatch(error, info) {
    // 展現出錯的UI
    this.setState({ hasError: true });
    // 將錯誤信息上報到日誌服務器
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // 能夠展現自定義的錯誤樣式
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

可是須要注意的是, error boundaries 並不會捕捉下面這些錯誤:

  • 事件處理器
  • 異步代碼
  • 服務端的渲染代碼
  • 在 error boundaries 區域內的錯誤

咱們能夠這樣使用 ErrorBoundary:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary

5.5 Vue 異常

Vue.config.errorHandler = (err, vm, info) => {
  console.error("經過vue errorHandler捕獲的錯誤");
  console.error(err);
  console.error(vm);
  console.error(info);
};

5.6 請求異常

以最經常使用的 HTTP 請求庫 axios 爲例,模擬接口響應 401 的狀況:

// 請求
axios.get(/api/test/401")
// 結果
Uncaught (in promise) Error: Request failed with status code 401
at createError (axios.js:1207)
at settle (axios.js:1177)
at XMLHttpRequest.handleLoad (axios.js:1037)

能夠看出來 axios 的異常能夠當作 Promise 異常來處理:

// 請求
axios.get("http://localhost:3000/api/uitest/sentry/401")
.then(data => console.log('接口請求成功', data))
.catch(e => console.log('接口請求出錯', e));
// 結果
接口請求出錯 Error: Request failed with status code 401
at createError (createError.js:17)
at settle (settle.js:18)
at XMLHttpRequest.handleLoad (xhr.js:62)

通常接口 401 就表明用戶未登陸,就須要跳轉到登陸頁,讓用戶進行從新登陸,但若是每一個請求方法都須要寫一遍跳轉登陸頁的邏輯就很麻煩了,這時候就會考慮使用 axios 的攔截器來作統一梳理,同理能統一處理的異常也能夠在放在攔截器裏處理。

// Add a response interceptor
axios.interceptors.response.use(
  function (response) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
  },
  function (error) {
    if (error.response.status === 401) {
      goLogin(); // 跳轉登陸頁
    } else if (error.response.status === 502) {
      alert(error.response.data.message || "系統升級中,請稍後重試");
    }
    return Promise.reject(error.response);
  }
);

5.7 總結

異常一共七大類,處理時需分清是致命錯誤仍是非致命錯誤。

  • 可疑區域增長 try-catch
  • 全局監控 JS 異常 window.onerror
  • 全局監控靜態資源異常 window.addEventListener
  • 捕獲沒有 catchPromise 異經常使用 unhandledrejection
  • Vue errorHandlerReact componentDidCatch
  • Axios 請求統一異常處理用攔截器 interceptors
  • 使用日誌監控服務收集用戶錯誤信息

6、異常上報

即便咱們前端開發完成後,會有一系列的 Web 應用的上線前的驗證,如自測、QA 測試、code review 等,以確保應用能在生產上沒有事故。

可是事與願違,不少時候咱們都會接到客戶反饋的一些線上問題,這些問題有時候多是你本身代碼的問題。這樣的問題通常可以在測試環境重現,咱們很快的能定位到問題關鍵位置。可是,不少時候有一些問題,咱們在測試中並未發現,但是在線上卻有部分人出現了,問題確確實實存在的,這個時候咱們測試環境又不能重現,還有一些偶現的生產的偶現問題,這些問題都很難定位到問題的緣由,讓咱們前端工程師頭疼不已。

而咱們不可能每次都遠程給用戶解決問題,或者讓用戶按 F12 打開瀏覽器控制檯把錯誤信息截圖給咱們吧。這時候,咱們不得不借助一些工具來解決這一系列使人頭疼的問題。

前端錯誤監控日誌系統就應用而生。當前端代碼在生產運行中出現錯誤的時候,第一時間傳遞給監控系統,從而第一時間定位而且解決問題。

有不少成熟的方案可供選擇: ARMS、fundebug、BadJS、Sentry。政採雲當前使用的是 Sentry 的開源版本,並結合業務進行一些改造:

  • 與構建系統結合,構建項目時自動生成 Sentry 項目,注入 Sentry 腳本
  • 客服端注入 Sentry 客戶端腳本後,按項目、頁面等不一樣粒度配置告警事件的過濾規則
  • 對接釘釘消息系統,將告警消息推送到訂閱羣
  • 過濾接口錯誤和優化 Promise 錯誤上報信息

後續也能夠單開一篇介紹介紹,如何結合開源的錯誤監控系統,搭建具備公司特點的監控體系。

推薦閱讀

動態表單之表單組件的插件式加載方案

編寫高質量可維護的代碼:優雅命名

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索