Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈 React Error Boundaries #3

Open
YuyingWu opened this issue Nov 20, 2019 · 0 comments
Open

浅谈 React Error Boundaries #3

YuyingWu opened this issue Nov 20, 2019 · 0 comments
Labels

Comments

@YuyingWu
Copy link
Owner

前几天面试候选人的时候,有提到用React 16.x重构了一个电商首页,对各个业务模块做更细颗粒拆解和代码组织,提升开发效率和降低维护成本,在这里要给小同学点个赞,有想法也有落地成果。后来问及“一个模块报错,搞挂了整个页面”的问题,大家都知道,如果页面的entry是index.js,在里面引入各个components的话,运行时只要有一行代码挂了,整个js都会挂掉。针对这个问题,除了前端同学的自测、QA同学的测试或者人肉的code review,有没有代码层面或者机制,做好模块间的错误隔离/处理/上报?比较遗憾的是,在生产环境下,候选人没有针对这个线上大流量页面做相关的处理,靠人肉规范,没能给出比较好的错误隔离的设计思路和实践方案。

其实相关的思路可以有很多种,模块/页面级的异常处理、commit前跑test case、提交后或者发布前headless检查页面报错,等等。

在代码异常处理上,一般我们会用try-catch来做:

try {
  ReactDOM.render(<App />, document.getElementById('root'));
}
catch(error) {
  console.error('Unknown error intercepted. error: ', error);
}

但可以发现,异常信息是捕获到了,但是代码运行就结束了,并没有从异常中“恢复”。

Why Error Boundaries?

过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时 产生 可能无法追踪的 错误。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

React 16官方文档 —— 错误边界 >>

笔者之前写过一篇《这!就是1688 PC首页》,里面有提到我们团队基于React 16.x的页面实践,一个 index.jsx 中引入子目录下的各种 components ,初期开发阶段,整个应用处于“裸奔”的状态,一个组件抛错误,整个页面都会挂掉。日常抛JS error的情况包括但不限于代码逻辑没注意写错了、数据接口挂了或者返回结构不如预期、多层数据获取的容错等。首页有19个组件和一些公用的函数,如果其中一处报错,会把整个页面搞挂,风险太大。最后选用了React 16的Error Boundaries(错误边界)API去做components之间的容错隔离/错误上报的。

React Error Boundaries in Action

异常捕获/处理 HOC

在里面主要做几件事:

  1. 异常捕获后的UI控制标记。通过react life cycle里的 getDerivedStateFromError,捕获运行时的异常,用一个state hasError 来标记当前组件是否报异常了,后续可以用这个state来控制展示传入还是降级的UI;
  2. 异常捕获后的回调action componentDidCatch,可以在里面放异常日志上传到服务器的逻辑,同时也支持自定义错误处理回调 this.props.errorHandler
  3. 异常捕获后的自定义降级UI。有个 placeholderthis.props 传进来,支持自定义降级UI。由于异常发生后,我们对该组件可以是默认处理不展示,或者展示为开发者自定义的展示,所以这里开放了placeholder支持自定义UI。
import React from 'react';
import wpo from '@ali/retcodelog';

class ErrorBoundary extends React.Component {
  static defaultProps = {
    placeholder: null,
  };

  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
    };
  }

  static getDerivedStateFromError() {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 自定义错误处理回调
    const { errorHandler } = this.props;
    errorHandler && errorHandler({ error, info });

    // 错误日志上报到服务器
    wpo.error('Error Handler', info && 
    info.componentStack ? 
    info.componentStack : 
    'Unexpect error from Error Handler');    
  }

  render() {
    const { placeholder } = this.props;
    const { hasError } = this.state;

    // 异常发生后的自定义降级(恢复)UI
    if (hasError) {
      return placeholder;
    }

    let tpl = this.props.children;

    return tpl;
  }
}

export default ErrorBoundary;

在应用中使用错误边界

一个应用中,可能会存在多个错误边界,如对整个应用App的全局错误边界,以及对每个模块使用的模块错误边界。

{/* global error boundary  */}
<ErrorBoundary errorHandler={() => { window.location.href = 'https://1688.com' }}>
  <App>
    {/* category error boundary  */}
    <ErrorBoundary placeholder={<StaticCategory />}>
      <Category />
    </ErrorBoundary>
  </App>
</ErrorBoundary>

全局或模块级错误边界可能看下图会更直观一些。

"multi-error-boundaries"

Wrapping It Up

回到文章的一开头,很多时候,我们在日常开发中,会不断地往前冲,开发很多酷炫的交互或者功能,有时可能忘记停下来,把更多的目光在质量保障上,本文提到的React Error Boundaries只是其中一个小的点。不同的设备环境、网络环境、用户操作路径,会不会在你不知道的情况下,我们的页面“坏”了,还是需要把这些信息收集起来,让我们看到“想当然”以外更多的东西。

共勉~

附录

值得注意的是,错误边界有一些无法捕获错误的场景:

事件处理
异步代码(如 setTimeoutrequestAnimationFrame 回调函数)
服务端渲染
ErrorBoundary自身抛出的异常(并非来自子组件)

@YuyingWu YuyingWu added the react label Nov 20, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant