React 16 中的異常處理

王下邀月熊_Chevalier發表於2017-07-27

React 16 中的異常處理翻譯自 React 官方文件,從屬於筆者的 React 與前端工程化實踐系列中的 React 元件分割與解耦章節;也可以使用 create-webpack-app 執行本部分示例 。

異常處理

在 React 15.x 及之前的版本中,元件內的異常有可能會影響到 React 的內部狀態,進而導致下一輪渲染時出現未知錯誤。這些元件內的異常往往也是由應用程式碼本身丟擲,在之前版本的 React 更多的是交託給了開發者處理,而沒有提供較好地元件內優雅處理這些異常的方式。在 React 16.x 版本中,引入了所謂 Error Boundary 的概念,從而保證了發生在 UI 層的錯誤不會連鎖導致整個應用程式崩潰;未被任何異常邊界捕獲的異常可能會導致整個 React 元件樹被解除安裝。所謂的異常邊界即指某個能夠捕獲它的子元素(包括巢狀子元素等)丟擲的異常,並且根據使用者配置進行優雅降級地顯示而不是導致整個元件樹崩潰。異常邊界能夠捕獲渲染函式、生命週期回撥以及整個元件樹的建構函式中丟擲的異常。
我們可以通過為某個元件新增新的 componentDidCatch(error, info) 生命週期回撥來使其變為異常邊界:

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>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

然後我們就可以如常使用該元件:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

componentDidCatch() 方法就好像針對元件的 catch {} 程式碼塊;不過 JavaScript 中的 try/catch 模式更多的是面向命令式程式碼,而 React 元件本身是宣告式模式,因此更適合採用指定渲染物件的模式。需要注意的是僅有類元件可以成為異常邊界,在真實的應與開發中我們往往會宣告單個異常邊界然後在所有可能丟擲異常的元件中使用它。另外值得一提的是異常邊界並不能捕獲其本身的異常,如果異常邊界元件本身丟擲了異常,那麼會冒泡傳遞到上一層最近的異常邊界中。
在真實地應用開發中有的開發者也會將崩壞的介面直接展示給開發者,不過譬如在某個聊天介面中,如果在出現異常的情況下仍然直接將介面展示給使用者,就有可能導致使用者將資訊傳送給錯誤的接受者;或者在某些支付應用中導致使用者金額顯示錯誤。因此如果我們將應用升級到 React 16.x,我們需要將原本應用中沒有被處理地異常統一包裹進異常邊界中。譬如某個應用中可能會分為側邊欄、資訊皮膚、會話介面、資訊輸入等幾個不同的模組,我們可以將這些模組包裹進不同的錯誤邊界中;這樣如果某個元件發生崩潰,會被其直屬的異常邊界捕獲,從而保證剩餘的部分依然處於可用狀態。同樣的我們也可以在異常邊界中新增錯誤反饋等服務介面以及時反饋生產環境下的異常並且修復他們。完整的應用程式碼如下所示:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }
  
  componentDidCatch(error, errorInfo) {
    // Catch errors in any components below and re-render with error message
    this.setState({
      error: error,
      errorInfo: errorInfo
    })
    // You can also log error messages to an error reporting service here
  }
  
  render() {
    if (this.state.errorInfo) {
      // Error path
      return (
        <div>
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: `pre-wrap` }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      );
    }
    // Normally, just render children
    return this.props.children;
  }  
}

class BuggyCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState(({counter}) => ({
      counter: counter + 1
    }));
  }
  
  render() {
    if (this.state.counter === 5) {
      // Simulate a JS error
      throw new Error(`I crashed!`);
    }
    return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
  }
}

function App() {
  return (
    <div>
      <p>
        <b>
          This is an example of error boundaries in React 16.
          <br /><br />
          Click on the numbers to increase the counters.
          <br />
          The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
        </b>
      </p>
      <hr />
      <ErrorBoundary>
        <p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
        <BuggyCounter />
        <BuggyCounter />
      </ErrorBoundary>
      <hr />
      <p>These two counters are each inside of their own error boundary. So if one crashes, the other is not affected.</p>
      <ErrorBoundary><BuggyCounter /></ErrorBoundary>
      <ErrorBoundary><BuggyCounter /></ErrorBoundary>
    </div>
  );
}



ReactDOM.render(
  <App />,
  document.getElementById(`root`)
);

相關文章