[译]React高级话题之Error Boundaries

4,527 阅读7分钟

前言

本文为意译,翻译过程中掺杂本人的理解,如有误导,请放弃继续阅读。

原文地址:Error Boundaries

在过去,React组件内部的javascript错误往往会让React内部的state变得不可用,并且会在下一次的渲染过程中产生模棱两可的错误信息。这些错误常常是由APP先前的错误所引起的,但是React并没有提供一个优雅的方案去在组件内部处理这种错误,并将APP恢复到正常的状态。

正文

介绍Error Boundaries

应用中局部UI中的javascript错误按理说不应该导致整个应用的崩溃。为了帮助React用户解决这种问题,React在16.x.x中引入了新的概念-“error boundary”。

什么是“error boundary”呢?“error boundary”就是一种能够捕获它的子组件树所产生的错误的React组件。在这种组件里,你能够把这些错误日志打印出来,又或者相比简单粗暴地把组件树崩溃后的界面呈现给用户,你可以呈现一个精心设计过的备用界面给用户(为了强调error boundary是一个组件,我后面的翻译过程中使用<Error boundary>来指代)。<Error boundary>能捕获在渲染过程中,所有子组件的constructors和生命周期函数里面发生的错误。

<Error boundary>不能捕获以下类型的错误:

  • 发生在事件处理器里面的。
  • 异步代码。例如 setTimeout,或者requestAnimationFrame的callbacks。
  • 服务端渲染
  • <Error boundary>本身抛出的错误。

从代码的层面来说,只要一个class component定义了static getDerivedStateFromError()componentDidCatch()方法中的一个,又或者两个都定义了,我们就说它是一个<Error boundary>。上面提到的两个方法其实是有分工的。一般来说,static getDerivedStateFromError()是不允许发生副作用的,故是负责呈现一个备用的UI给用户。componentDidCatch()允许发生副作用,故负责打印错误日志,发送错误日志到远程服务器等。如下:

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

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 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>

<Error boundary>使用起来就像catch {}语句,只不过它是用于React component而已。只有class component才能成为<Error boundary>。在实际应用中,大多数情况,你只需要定义一个<Error boundary>,然后到处使用它。

注意,<Error boundary>是不能捕获自己所产生的错误,只能捕获在它之下的组件树所产生的错误。在<Error boundary>嵌套使用的情况下,如果某个<Error boundary>不能渲染一些错误信息(调用static getDerivedStateFromError()失败?),那么这个错误就会往上冒泡到层级最近的个<Error boundary>。这也是catch{}语句在javascript里面的执行机制。

在线Demo

查看如何在React 16版本中定义和使用 error boundary

在哪里“放置”Error Boundaries

在组件树中“放置”<Error boundary>的粒度完全取决于你。你可以把你最顶级的route component包裹在<Error boundary>中,然后让<Error boundary>呈现一个备用的界面给用户,例如“Something went wrong”。服务端渲染经常就是这样应对应用崩溃的。你也可以把多个组件分开包裹在<Error boundary>中,以此隔离局部UI之间的影响。

错误捕获的新行为

在React 16版本中,任何没有被<Error boundary>捕获的错误都会导致整一颗React组件树的卸载。这么做,是有我们自己的考量的。

关于这个决定,我们是有争论过的。但是,依据我们以往的经验来看,遗留一个不正常(corrupted)的页面给用户比完全不显示更糟糕。举个例子,在Messenger这种的产品中,把一个不正常的页面给用户会导致信息错发给别的人。同样的,对于一些涉及到支付的应用,情况会更糟糕。因为涉及到钱的问题都是大问题啊,所以说宁愿什么都不显示,也不要显示一个错误的金额数字给用户。

这种错误捕获的新行为对你是有影响的。假如你已经迁移到React 16版本上面来,你应该去检查一下你的应用,看看哪些地方有可能导致应用崩溃。然后,在对应的地方添加<Error boundary>,通过备用UI界面来提供一个更好的用户体验。

我们来看看,Facebook的Messenger是怎么做的。Messenger分别将sidebar的内容,info panel,conversation log和 message input等区域包裹在不用的<Error boundary>中去。如果这些区域中的某个组件发生了错误,那么影响范围也就仅仅限定在这个区域中而已,别的区域将不会受到影响的。

使用<Error boundary>的同时,我们也鼓励你去使用js错误报告服务(或自己搭建一个服务器),好让你能在第一时间了解到在生产环境所产生的未处理异常,并及时修复它。

组件栈的追踪

在开发环境下,React 16会将渲染过程中出现的所有错误打印在控制台。这也包括了那些应用程序静默处理的错误(even if the application accidentally swallows them.)。除了错误信息和js的调用栈,React 16还会打印组件树的栈追踪。现在,你可以看到错误是发生在组件树中的哪个位置了。

在组件栈追踪里面,你也以可查看错误组件代码所在的文件和行号。在由create-react-app创建的项目里面,这是默认行为:

如果你没有使用create-react-app来创建你的应用脚手架,那么你也可以通过给你的Babel手动地添加这个插件来实现这个功能。注意,这个功能只应该在开发环境使用,假如你是通过给Babel配置来实现这个功能的话,那么你记得在生产环境下禁用它。

注意:在组件追踪栈上显示的组件名是基于Function.name属性的。假如你需要支持一些没有在原生层级实现了这个特性的老浏览器或者设备(例如:IE11),那么就可以考虑引用polyfill(function.name-polyfill)到你的代码中。除此之外,你也可以显式地指定组件的displayName属性的值。

那try/catch怎么办啊?

try / catch 是挺好用的,但是也仅仅针对命令式代码好用而已:

try {
  showButton();
} catch (error) {
  // ...
}

然而,React组件是通过声明式的编码来指定我们想要渲染什么:

<Button />

<Error boundary>保留了React天生的声明式特性,使用它的结果也会如你所愿的。举个例子说,在componentDidUpdate方法里面发生了一个错误,即使这个错误是由层级很深的组件在调用setState的时候引起的,这个错误还是会正确地冒泡到离它最近的那个<Error boundary>中去,并被它所捕获。

那Event Handlers怎么办啊?

<Error boundary>无法捕获发生在事件处理器中的错误。

React不需要<Error boundary>把应用从事件处理器中发生的错误所引起的崩溃中恢复过来。不像render方法和生命周期方法,事件处理器的调用并没有发生在渲染过程中。但是,假如在事件处理器中抛出错误了,React也知道该如何显示它。

假设,你需要在事件处理器中捕获一个错误,你可以使用常规的try/catch语句:

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

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

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <div onClick={this.handleClick}>Click Me</div>
  }
}

React 15到React 16的命名更改

React 15包含了一个叫unstable_handleError的方法。这个方法对error boundaries功能的实现提供了一些有限的支持。这个方法在React 16 beta 版之后就不可用了。你可以将它替换为componentDidCatch方法。为了支持API迁移,我们提供了一个叫codemod的类库帮助你自动升级。