React 異常處理

熊建剛發表於2019-03-01

在React 15.x版本及之前版本中,元件內的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 {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Meet Some Errors.</h1>;
    }
    return this.props.children;
  }
}複製程式碼

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

<ErrorBoundary>
  <App />
</ErrorBoundary>複製程式碼

ComponentDidCatch

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

ComponentDidCatch(error, info)複製程式碼
  1. error:應用丟擲的異常;

    異常物件
    異常物件
  2. info:異常資訊,包含ComponentStack屬性對應異常過程中冒泡的元件棧;

    異常資訊元件棧
    異常資訊元件棧

判斷元件是否新增componentDidCatch生命週期方法,新增了,則呼叫包含異常處理的更新渲染元件方法:

if (inst.componentDidCatch) {
  this._updateRenderedComponentWithErrorHandling(
    transaction,
    unmaskedContext,
  );
} else {
  this._updateRenderedComponent(transaction, unmaskedContext);
}複製程式碼

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

 /**
   * Call the component`s `render` method and update the DOM accordingly.
   *
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
_updateRenderedComponentWithErrorHandling: function(transaction, context) {
  var checkpoint = transaction.checkpoint();
  try {
    this._updateRenderedComponent(transaction, context);
  } catch (e) {
    // Roll back to checkpoint, handle error (which may add items to the transaction),
    // and take a new checkpoint
    transaction.rollback(checkpoint);
    this._instance.componentDidCatch(e);     

    // Try again - we`ve informed the component about the error, so they can render an error message this time.
    // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
    this._updateRenderedComponent(transaction, context);
  }
},複製程式碼

具體原始碼見Github

unstable_handleError

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

業務專案中的異常邊界

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

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

點此傳送檢視例項

元件外異常

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

  1. window.onerror捕獲全域性JavaScript異常;

    // 在應用入口元件內呼叫異常捕獲
    componentWillMount: function () {
      this.startErrorLog();
    }
    startErrorLog:function() {
      window.onerror = (message, file, line, column, errorObject) => {
        column = column || (window.event && window.event.errorCharacter);
        const stack = errorObject ? errorObject.stack : null;

        // trying to get stack from IE
        if (!stack) {
          var stack = [];
          var f = arguments.callee.caller;
          while (f) {
             stack.push(f.name);
             f = f.caller;
          }
          errorObject[`stack`] = stack;
        }

        const data = {
          message:message,
          file:file,
          line:line,
          column:column,
          errorStack:stack,
        };

        // here I make a call to the server to log the error
        reportError(data);

        // the error can still be triggered as usual, we just wanted to know what`s happening on the client side
        // if return true, this error will not be console log out  
        return false;
        }
    }複製程式碼
  2. try, catch手動定位包裹易出現異常的邏輯程式碼;

    class Home extends React.Component {
      constructor(props) {
        super(props);
        this.state = { error: null };
      }

      handleClick = () => {
        try {
          // Do something that could throw
        } catch (error) {
          this.setState({ error });
        }
      }

      render() {
        if (this.state.error) {
          return <h1>Meet Some Errors.</h1>
        }
        return <div onClick={this.handleClick}>Click Me</div>
      }
    }複製程式碼

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

參考

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

相關文章