Skip to content

如何在React应用中建立错误处理层

https://juejin.cn/post/7293182931005620233

写在前面

在实际的业务场景中,react应用一定会涉及到错误处理的情况。

默认情况下,react应用出现fetal error时,浏览器上显示的是一个空白页面, 这对于用户来说很不友好———他们并不知道发生了什么以及为什么发生。

对于前端开发者而言,我们应该如何去处理这种错误处理,避免影响用户体验呢?最简单最有效的方法,就是在我们的react应用中添加一个错误处理层(Error Handling Layer)。

错误处理层能够将所有错误处理逻辑集中在一起,允许我们捕获错误并向用户显示清晰而有用的错误消息。 这可以帮助react应用避免因为崩溃而导致的浏览器白屏显示,从而改善整体用户体验。

接下来就让我们详细了解错误处理层是什么,理解为什么React应用需要有一个错误处理层,以及如何在React中实现错误处理层。

react应用中什么是错误处理层?

错误处理层是react应用中的一部分,它将错误处理逻辑的代码封装并集中在一个地方,负责检测和处理react应用中发送的错误。

在React应用中,错误处理层通常负责以下功能:

错误检测:拦截react应用程序在执行期间发生的错误和异常。

错误处理:在检测到错误后,它将优雅的处理掉这些错误,特别是防止react应用程序因为一些错误导致崩溃而在浏览器端显示白屏,并且能够在浏览器端向用户显示自定义的错误页面。

在背后,错误处理层还可以记录错误并将错误日志报告给错误监控服务。

react应用中为什么需要错误处理层?

如前文所述,react应用中默认对异常错误的处理方式就是卸载整个应用,浏览器显示白屏,它没有内置的机制来处理这些运行过程中发生的错误。

可以想象,用户遇到应用白屏的情况,绝对会是一种懵逼状态,他们完全不知道发生了什么。 因此,react默认的错误处理方式不是一个理想的解决方案,我们才需要在react应用中建立一个错误处理层。

详细地来说,建立错误处理层有以下好处:

避免崩溃:如果有错误发生了,错误处理层会拦截它,并按照我们的意愿来处理错误,防止应用程序因未处理的错误而崩溃。

改善用户体验:错误处理层无论何时检测到错误,最终都会让应用向用户显示一个漂亮的错误页面,并展示错误信息,这对用户来说十分友好。

集中错误处理逻辑:能够将react应用中所有的错误处理逻辑集中在一起,并在react应用中集成错误日志记录和监控,开发者们可以方便的收集这些错误信息,然后使用这些信息来识别错误的根源、调试和修复错误。

下面我们就来看看如何在react应用中添加错误处理层。

使用错误边界(Error Boundary)来实现错误处理层

从react 16开始,官方提供了Error Boundary来进行错误处理。

Error Boundary是一个特殊的react组件,它可以捕获子组件树中任何地方发生的JS错误,并且在错误发生时显示备用UI组件,从而优雅地处理错误并避免应用出现致命的崩溃。

需要注意的是,标准Error Boundary只能捕获在组件渲染时、生命周期方法中和组件构造函数中的错误,其它时候的错误它将捕获不到。

接下来让我们一步一步在React应用中使用Error Boundary来创建错误处理层!

  1. 创建一个Error Boundary组件

要在React中创建一个Error Boundary,我们需要定义一个类组件,它包括以下内容:

状态变量hasError:用于确定错误边界是否拦截了错误。

静态方法getDerivedStateFromError:一个React生命周期方法,在后代组件抛出错误后调用,直译为 从错误中派生状态。

该方法返回的值将更新Error Boundary组件的状态。

方法componentDidCatch:一个特殊的React生命周期方法,当Error Boundary捕获错误时调用。

我们可以用它来记录错误日志并上报。 一个带有if-else逻辑的render()方法:在出现错误的情况下,它返回备用UI组件。否则,它将显示由Error Boundary包装的子组件。

注意,React只在类组件上支持关键的生命周期方法getDerivedStateFromError()和componentDidCatch()。这意味着无法将错误边界编写为函数式组件。

定义Error Boundary组件:

tsx
import React from 'react';
import ErrorPage from './ErrorPage';

export default class StandardErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: undefined
    };
  }

  // 当出现错误时更新组件状态
  static getDerivedStateFromError(error) {
    // 指定Error Boundary捕获错误
    return {
      hasError: true,
      error
    };
  }

  // 定义捕获错误时要做什么
  componentDidCatch(error, errorInfo) {
    // 记录错误日志
    console.log('Error caught!');
    console.error(error);
    console.error(errorInfo);
  }

  render() {
    // 错误出现时ErrorPage
    if (this.state.hasError) {
      return <ErrorPage />;
    } else {
      // 正常显示
      return this.props.children;
    }
  }
}

当在它的任何子组件中发生错误时,StandardErrorBoundary会拦截、记录它,并将hasError设置为true,然后呈现备用UI组件。

定义ErrorPage 组件:

tsx
export default function ErrorPage(props) {
  return (
    <div className="error-page">
      <div className="oops">Oops!</div>
      <div className="message">Something went wrong...</div>
    </div>
  );
}

然后我们可以根据自己的喜好来定制备用UI组件ErrorPage样式就行了。 值得注意的是,一个有效的ErrorPage组件告诉用户发生了什么就行了,不需要过多的细节来让用户担心。

  1. 如何使用Error Boundary组件

我们在react应用中用上面定义的StandardErrorBoundary组件包裹住顶级组件就行了:

text
<StandardErrorBoundary>
    <App />
</StandardErrorBoundary>

现在,react应用中发生的任何与react相关的错误都将被Error Boundary拦截和处理。

注意:react应用中可以有多个Error Boundary,每一个Error Boundary都负责处理不同组件的子树中的错误。

但是为了将错误逻辑集中在一个地方,还是建议只使用一个顶级错误边界比较好。

  1. Error Boundary的局限性

Error Boundary是一个很好的工具,但是它也有一些局限性,react官方文档中提到,Error Boundary不能捕获到下列的异常错误:

  • 异步代码
  • 事件处理函数
  • 服务器组件
  • Error Boundary自身

考虑到JavaScript严重依赖事件回调和异步代码,Error Boundary的这些限制有可能带来一些问题。

使用类式错误边界组件

tsx
import { Component } from 'react';

export class CustomErrorBoundary extends Component {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: any, errorInfo: any) {
    console.log(error, errorInfo);
  }

  render() {
    if ((this.state as any).hasError) {
      return <p>something went wrong.</p>;
    }

    return (this.props as any).children;
  }
}
tsx
import { CustomErrorBoundary } from './CustomErrorBoundary';

function BuggyComponent({ isError }: { isError: boolean }) {
  if (isError)
    throw new Error('I crashed during rendering!');

  return <div>这个组件将不会被渲染。</div>;
}

export default function CustomErrorBoundaryDemo() {
  return (
    <CustomErrorBoundary>
      <BuggyComponent isError />
    </CustomErrorBoundary>
  );
}

使用函数式错误边界组件

目前还无法将错误边界编写为函数式组件,但是可以借助第三方库(react-error-boundary)来实现。

bash
# npm
npm install react-error-boundary

# pnpm
pnpm add react-error-boundary

# yarn
yarn add react-error-boundary
tsx
import { ErrorBoundary } from "react-error-boundary";

function BuggyComponent({ isError }: { isError: boolean }) {
  if (isError) throw new Error("I crashed during rendering!");

  return <div>这个组件将不会被渲染。</div>;
}

export default function ReactErrorBoundary() {
  return (
    <>
      <ErrorBoundary fallback={<div>something went wrong!</div>}>
        <BuggyComponent isError />
      </ErrorBoundary>
    </>
  );
}

使用react-error-boundary来实现错误处理层

在react官方文档中,也推荐使用react-error-boundary来实现react应用中的错误处理。

react-error-boundary是一个npm react库,它提供了一种简单可靠的方法来使用错误边界来处理错误。

它简化了创建错误边界的过程,并提供了有效的解决方案来克服标准错误边界的局限性。

我们通过npm install 将react-error-boundary添加到我们的项目中来

text
npm install react-error-boundary

接下来我详细讲讲如何使用它。

  1. 创建Error Boundary组件

react-error-boundary库提供了一个ErrorBoundary组件,它暴露了几个prop属性来构建错误边界组件。

下面是用react-error-boundary创建的错误边界:

tsx
import { ErrorBoundary } from 'react-error-boundary';
import ErrorPage from './ErrorPage';

export default function ReactErrorBoundary(props) {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorPage}
      onError={(error, errorInfo) => {
        // log the error
        console.log('Error caught!');
        console.error(error);
        console.error(errorInfo);
        // record the error in an APM tool...
      }}
    >
      {props.children}
    </ErrorBoundary>
  );
}

FallbackComponent属性,提供了一个在错误时渲染的备用UI组件。

当错误发生时,也会触发onError事件,执行我们定义的事件处理函数。

如果没有错误发生,将显示它所包裹的子组件。

相比前面用官方的标准的Error Boundary组件,通过第三方库来创建错误边界,代码更简洁明了。

和前文一样,我们用我们的错误边界组件包裹我们顶级组件就行了:

text
<ReactErrorBoundary>
    <App />
</ReactErrorBoundary>

用react-error-boundary构建的错误边界组件既可以是函数组件,也可以是类组件。

我们不再局限于类组件,可以根据自己的目标和偏好做出最佳选择。

  1. 发生错误后重置应用

react-error-boundary 提供了一个onReset的prop属性,它接受一个回调函数,ErrorBoundary组件会自动将这个方法传递给备用UI组件。

现在我们来更新我们前文定义的ReactErrorBoundary组件:

tsx
import { ErrorBoundary } from 'react-error-boundary';
import ErrorPage from './ErrorPage';

export default function ReactErrorBoundary(props) {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorPage}
      onError={(error, errorInfo) => {
        // log the error
        console.log('Error caught!');
        console.error(error);
        console.error(errorInfo);
      }}
      onReset={() => {
        // reloading the page to restore the initial state
        // of the current page
        console.log('reloading the page...');
        window.location.reload();
        // other reset logic...
      }}
    >
      {props.children}
    </ErrorBoundary>
  );
}

onReset回调函数所做的只是通过js重新加载当前页面,这是最基本的错误恢复逻辑。

在更复杂的应用程序中,您可能需要将应用的状态恢复到前一个点或更新Redux状态。

默认情况下,ErrorBoundary会向备用UI组件传递以下两个prop:

Error:存储由错误边界捕获的错误对象。

resetErrorBoundary:包含传递给onReset prop属性的回调函数(如果定义的话)。

现在,我们可以试试添加一个重试按钮到ErrorPage,如下所示:

tsx
import './ErrorPage.css';

export default function ErrorPage(props) {
  return (
    <div className="error-page">
      <div className="oops">Oops!</div>
      <div className="message">Something went wrong...</div>
      {props.resetErrorBoundary && (
        <div>
          <button className="retry-button" onClick={props.resetErrorBoundary}>
            Try Again!
          </button>
        </div>
      )}
    </div>
  );
}

现在,出现错误时显示的回退页面上将显示“Tra Again”按钮。当用户单击它时,将执行onReset函数并重新加载当前页面。

如果应用是在用户操作过程中出现的错误,那么刷新页面,应用将恢复正常。

  1. 处理异步代码的错误

react-error-boundary提供了useErrorHandler()

hook,这个hook能够允许我们捕获到到传统的React错误边界无法捕获到的error,因此在API请求、事件处理程函数中,我们可以使用useErrorHandler()来捕获错误。

下面就是一个在异步请求代码中使用useErrorHandler() hook来捕获异常的示例:

tsx
import { useEffect } from 'react';
import { useErrorHandler } from 'react-error-boundary';

export default function MyPageComponent(props) {
  const handleError = useErrorHandler();

  useEffect(() => {
    // API call
    fetchSomeData().then(() => {
      // ...
    }).catch((e) => {
      // 将错误传递给ErrorBoundary
      handleError(e);
    });
  }, []);

  // return ...
}

类似地,我们可以在标准错误边界捕获不到错误的任何情况下采用useErrorhandler()。

另外,请记住,不管我们是用react-error-boundary实现的错误边界还是标准实现的错误边界,useErrorhandler()可以将错误传递过去。

结语

在本文中,我们了解了错误处理层是什么,错误处理层的好处,以及如何将其集成到React应用中来。

错误处理层其实就是一个错误边界组件,它能够捕获错误然后优雅地处理错误。

这样我们就能够将错误处理相关的逻辑全部整合到react应用中一个地方,从而让我们调试、回溯问题更加方便容易。

得益于第三方库react-error-boundary,我们能够弥补官方提供的标准错误边界组件的不足,让我们能够更加游刃有余的去实现react应用中错误处理层。

React 错误边界指南

虽然在错误到达生产环境之前捕获错误是理想的,但是其中一些错误(例如网络错误)可能会通过测试而影响用户。

如果你的 React 组件没有正确地捕捉到第三方库或 React Hooks 抛出的错误,这样的错误要么导致 React 生命周期崩溃,要么到达主执行线程的顶层,导致“白屏”场景。

在React 16中,没有捕捉到的错误[…]将导致整个 React 组件树被卸载

您的应用程序通过提供适当的可视化反馈和潜在操作(例如:重试机制)来优雅地处理此类错误是至关重要的

幸运的是,使用 React API 实现这样的用户体验模式只需要很少的工作,对于最高级的用户体验,还需要轻量级 React 库的帮助。

在 React Hooks 调用周围使用 JavaScript 的 try-catch 是行不通的,因为它们的执行是异步的。然而,React API 提供了错误边界机制来捕获组件中可能“冒出来”的所有类型的错误。

例如,如果 <ComponentA /> 被封装在一个 React Error 边界中,错误传播将在 Error boundary 级别停止,防止 React App 崩溃 本文将介绍如何在应用程序中实现错误边界,从简单的错误捕获到显示可视化反馈和提供重试机制。

简单错误边界的捕获和报告错误

在它复杂的名字背后,Error Boundary 只是一个componentDidCatch(error) 方法的普通类 React 组件:

text
class ErrorBoundarySimple extends React.Component {

  componentDidCatch(error) {
    // 报告错误到您最喜欢的错误跟踪工具(例如:Sentry, Bugsnag)
  }

  render() {
    return this.props.children;
  }

}

注意:React 还没有提供基于 hook 的替代方法来实现错误边界。

一旦错误到达我们的 MyErrorBoundary 组件,componentDidCatch() 类方法就会被调用,这允许我们防止 React 应用程序崩溃并将错误转发到我们的错误报告工具。

让我们让 <ErrorBoundarySimple> 更加友好,在错误被抛出时添加简单的可视化反馈。

为此,我们向 ErrorBoundarySimple 添加一些状态,并使用 getDerivedStateFromError() 方法,如下所示

text
class ErrorBoundarySimple extends React.Component {
  state = { hasError: false };

  componentDidCatch(error: unknown) {
    // 报告错误到您最喜欢的错误跟踪工具(例如:Sentry, Bugsnag)
    console.error(error);
  }

  static getDerivedStateFromError(error: unknown) {
    // 更新状态,使下一次渲染将显示回退 UI
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <p>Failed to fetch users.</p>;
    }

    return this.props.children;
  }
}

React 期望getDerivedStateFromError() 方法在发生错误时返回应用于 <ErrorBoundarySimple> 的状态值,从而使我们的 UI 提供视觉反馈。

错误边界也可以嵌套,以提供更多上下文化的反馈。例如,在这个 React 应用树中,我们可能想根据崩溃的内容提供不同的反馈。

例如,当聊天崩溃和 TodoList 崩溃时,我们可能希望提供不同的反馈,但仍然在应用程序级别处理任何类型的崩溃。

我们可以引入多个边界来实现这一点: 通过上面的设置,<Chat> 组件(或它的子组件)中的任何错误都将被捕获在包装 <Chat> 组件的错误边界(而不是“App”错误边界)中,允许我们给出上下文化的可视化反馈。

但是,来自所有 <App> 后代的任何错误(不包括<Chat><TodoList>)将被" App "错误边界捕获。

仅用几行代码,我们就通过优雅地处理应用程序中的错误,极大地改善了用户体验。然而,这种简单的错误边界实现确实有局限性。

首先,根据 React 文档,错误边界不会捕获以下错误:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调)
  • 服务器端渲染
  • 抛出在错误边界本身(而不是其子边界)中的错误

而且,前面展示的错误边界没有为用户提供从错误中恢复的任何操作,例如,通过重试机制。

在下一节中,我们将了解如何利用 react-error-boundary 库来处理所有这些边界情况。

高级错误边界的捕获所有错误和重试机制

现在,让我们通过捕捉各种错误并向用户公开恢复操作来提供高级的错误处理用户体验。为此,我们将使用 react-error-boundary 库,该库可以按如下方式安装:

sh
npm install --save react-error-boundary

yarn add react-error-boundary

提供重试机制

我们新定义了一个 <Users> 组件,该组件在50%的情况下无法加载用户。

让我们使用 react-error-boundary 来正确捕获错误并提供重试机制

tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { Users } from './Users';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <p>Failed to load users:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

export default function App(): JSX.Element {
  return (
    <div className="App">
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        {/* Users 加载失败的概率为50% */}
        <Users />
      </ErrorBoundary>
    </div>
  );
}

<ErrorBoundary/> 接受一个强制的 FallbackComponent = prop,它应该是发生错误时将呈现的 react 组件或 JSX。

如果是一个组件,这个FallbackComponent=function 将接收 FallbackProps:

error 可用于显示错误。

resetErrorBoundary 是一个回调函数,用于重置错误状态并重新渲染子组件。

还可以提供 ononError prop,将错误转发到您最喜欢的错误报告工具(例如:Sentry)。

react-error-boundary 「文档」 展示了如何利用其他props(例如:onReset=)来处理更高级的场景。

捕获所有错误

如前所述,错误边界不会捕获以下错误:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调)

因为这种错误发生在 React 呈现生命周期之外,所以不会调用 错误边界。

同样,通过提供 handleError() hook 来帮助捕获与事件相关的和异步错误,庆幸的是 react-error-boundary已经给我们提供了

tsx
import { useErrorHandler } from 'react-error-boundary';

function Greeting() {
  const [greeting, setGreeting] = React.useState(null);
  const handleError = useErrorHandler();

  function handleSubmit(event) {
    event.preventDefault();
    const name = event.target.elements.name.value;
    fetchGreeting(name).then(
      newGreeting => setGreeting(newGreeting),
      error => handleError(error),
    );
  }

  return greeting
    ? (
      <div>{greeting}</div>
    )
    : (
      <form onSubmit={handleSubmit}>
        <label>Name</label>
        <input id="name" />
        <button type="submit">get a greeting</button>
      </form>
    );
}

在 handleSubmit() 函数中发生的错误不会被 React 呈现生命周期捕获。

因此,我们使用 React-error-boundary 的 useErrorHandler() 提供的 handleError 函数在 React 生命周期中重新抛出错误,以便最近的 ErrorBoundary 可以捕获它。

小结

React Error Boundary 是一种优雅地处理 React 应用程序中任何类型错误的直接方法。

好的产品应该防止错误到达生产,但也应该使用错误边界为用户提供上下文反馈和恢复操作,以防出现意外错误。

Contributors

作者:Long Mo
字数统计:5k 字
阅读时长:17 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs