React異常處理

在React 0.15版本及之前版本中,組件內的UI異常將中斷組件內部狀態,導致下一次渲染時觸發隱藏異常。React並未提供友好的異常捕獲和處理方式,一旦發生異常,應用將不能很好的運行。而React 16版本有所改進。本文主旨就是探尋React異常捕獲的現狀,問題及解決方案。

歡迎訪問我的個人博客

前言

我們期望的是在UI中發生的一些異常,即組件內異常(指React 組件內發生的異常,包括組件渲染異常,組件生命周期方法異常等),不會中斷整個應用,可以以比較友好的方式處理異常,上報異常。在React 16版本以前是比較麻煩的,在React 16中提出了解決方案,將從異常邊界(Error Boundaries)開始介紹。

異常邊界

所謂異常邊界,即是標記當前內部發生的異常能夠被捕獲的區域範圍,在此邊界內的JavaScript異常可以被捕獲到,不會中斷應用,這是React 16中提供的一種處理組件內異常的思路。具體實現而言,React提供一種異常邊界組件,以捕獲並列印子組件樹中的JavaScript異常,同時顯示一個異常替補UI。

組件內異常

組件內異常,也就是異常邊界組件能夠捕獲的異常,主要包括:

  1. 渲染過程中異常;
  2. 生命周期方法中的異常;
  3. 子組件樹中各組件的constructor構造函數中異常。

其他異常

當然,異常邊界組件依然存在一些無法捕獲的異常,主要是非同步及服務端觸發異常:

  1. 事件處理器中的異常;
  2. 非同步任務異常,如setTiemout,ajax請求異常等;
  3. 服務端渲染異常;
  4. 異常邊界組件自身內的異常;

異常邊界組件

前面提到異常邊界組件只能捕獲其子組件樹發生的異常,不能捕獲自身拋出的異常,所以有必要注意兩點:

  1. 不能將現有組件改造為邊界組件,否則無法捕獲現有組件異常;
  2. 不能在邊界組件內涉及業務邏輯,否則這裡的業務邏輯異常無法捕獲;

很顯然,最終的異常邊界組件必然是不涉及業務邏輯的獨立中間組件。

那麼一個異常邊界組件如何捕獲其子組件樹異常呢?很簡單,首先它也是一個React組件,然後添加ComponentDidCatch生命周期方法。

實例

創建一個React組件,然後添加ComponentDidCatch生命周期方法:

class ErrorBoundary extends React.Component {n constructor(props) {n super(props);n this.state = { hasError: false };n }nn componentDidCatch(error, info) {n // Display fallback UIn this.setState({ hasError: true });n // You can also log the error to an error reporting servicen logErrorToMyService(error, info);n }nn render() {n if (this.state.hasError) {n // You can render any custom fallback UIn return <h1>Meet Some Errors.</h1>;n }n return this.props.children;n }n}n

接下來可以像使用普通React組件一樣使用該組件:

<ErrorBoundary>n <App />n</ErrorBoundary>n

ComponentDidCatch

這是一個新的生命周期方法,使用它可以捕獲子組件異常,其原理類似於JavaScript異常捕獲器try, catch

ComponentDidCatch(error, info)n

  1. error:應用拋出的異常;

  1. info:異常信息,包含ComponentStack屬性對應異常過程中冒泡的組件棧;

判斷組件是否添加componentDidCatch生命周期方法,添加了,則調用包含異常處理的更新渲染組件方法:

if (inst.componentDidCatch) {n this._updateRenderedComponentWithErrorHandling(n transaction,n unmaskedContext,n );n} else {n this._updateRenderedComponent(transaction, unmaskedContext);n}n

_updateRenderedComponentWithErrorHandling裡面使用try, catch捕獲異常:

/**n * Call the components `render` method and update the DOM accordingly.n *n * @param {ReactReconcileTransaction} transactionn * @internaln */n_updateRenderedComponentWithErrorHandling: function(transaction, context) {n var checkpoint = transaction.checkpoint();n try {n this._updateRenderedComponent(transaction, context);n } catch (e) {n // Roll back to checkpoint, handle error (which may add items to the transaction),n // and take a new checkpointn transaction.rollback(checkpoint);n this._instance.componentDidCatch(e); nn // Try again - weve informed the component about the error, so they can render an error message this time.n // If this throws again, the error will bubble up (and can be caught by a higher error boundary).n this._updateRenderedComponent(transaction, context);n }n},n

具體源碼見Github

unstable_handleError

其實異常邊界組件並不是突然出現在React中,在0.15版本中已經有測試React 15 ErrorBoundaries,源碼見Github。可以看見在源碼中已經存在異常邊界組件概念,但是尚不穩定,不推薦使用,從生命周期方法名也可以看出來:unstable_handleError,這也正是ComponentDidCatch的前身。

業務項目中的異常邊界

前面提到的都是異常邊界組件技術上可以捕獲內部子組件異常,對於業務實際項目而言,還有需要思考的地方:

  1. 異常邊界組件的範圍或粒度:是使用異常邊界組件包裹應用根組件(粗粒度),還是只包裹獨立模塊入口組件(細粒度);
  2. 粗粒度使用異常邊界組件是暴力處理異常,任何異常都將展示異常替補UI,完全中斷了用戶使用,但是確實能方便的捕獲內部所有異常;
  3. 細粒度使用異常邊界組件就以更友好的方式處理異常,局部異常只會中斷該模塊的使用,應用其他部分依然正常不受影響,但是通常應用中模塊數量是很多的,而且具體模塊劃分到哪一程度也需要開發者考量,比較細緻;

點此傳送查看實例

組件外異常

React 16提供的異常邊界組件並不能捕獲應用中的所有異常,而且React 16以後,所有未被異常邊界捕獲的異常都將導致React卸載整個應用組件樹,所以通常需要通過一些其他前端異常處理方式進行異常捕獲,處理和上報等,最常見的有兩種方式:

  1. window.onerror捕獲全局JavaScript異常;

// 在應用入口組件內調用異常捕獲ncomponentWillMount: function () {n this.startErrorLog();n}nstartErrorLog:function() {n window.onerror = (message, file, line, column, errorObject) => {n column = column || (window.event && window.event.errorCharacter);n const stack = errorObject ? errorObject.stack : null;nn // trying to get stack from IE if (!stack) {n var stack = [];n var f = arguments.callee.caller;n while (f) {n stack.push(f.name);n f = f.caller;n }n errorObject[stack] = stack;n }nn const data = {n message:message,n file:file,n line:line,n column:column,n errorStack:stack,n };nn // here I make a call to the server to log the errorn reportError(data);nn // the error can still be triggered as usual, we just wanted to know whats happening on the client side // if return true, this error will not be console log out return false;n }n}n

  1. try, catch手動定位包裹易出現異常的邏輯代碼;

class Home extends React.Component {n constructor(props) {n super(props);n this.state = { error: null };n }nn handleClick = () => {n try {n // Do something that could thrown } catch (error) {n this.setState({ error });n }n }nn render() {n if (this.state.error) {n return <h1>Meet Some Errors.</h1>n }n return <div onClick={this.handleClick}>Click Me</div>n }n}n

常見的開源異常捕獲,上報庫,如sentry,badjs等都是利用這些方式提供常見的JavaScript執行異常。

參考

  1. Error Boundaries
  2. Try Catch in Component
  3. Handle React Errors in v15

推薦閱讀:

Learn CSS Animations: From A Designer's Perspective
#研發解決方案#共享能力的數屏
創建 React 動畫的五種方式
web 開發中無處不在的狀態機

TAG:React | 异常处理 |