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中,在0.15版本中已經有測試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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章