React:在应用程序中统一处理错误(react-error-boundary)

140 阅读7分钟

React:在应用程序中统一处理错误(react-error-boundary)

引言

在前一篇文章中,我们探讨了如何使用react自带的错误边界机制Error Boundary进行错误处理。Error Boundary是React中用于捕获渲染错误的机制,但它有一些局限性,例如无法处理事件处理程序中的错误或异步操作中的错误。为了解决这些问题,我们将介绍一个名为react-error-boundary的库,它提供了一个更为灵活的错误处理方案,能够轻松捕获组件中的错误、事件处理程序中的错误以及异步操作中的错误。

环境

  • 操作系统:Windows 10
  • Vite版本:v2.7.2
  • Node版本:v19.0.0
  • React版本:v19.0.0
  • TypeScript版本:v5

react-error-boundary

react-error-boundary是由Facebook的React核心团队成员Brian Vaughn开发的一个库,旨在简化错误处理。它能够处理组件渲染错误、事件处理程序中的错误以及异步操作中的错误,极大地提升了开发者的体验。

安装方法

可以通过以下命令安装react-error-boundary

npm install react-error-boundary

创建一个没有错误的示例程序

首先,我们将创建一个简单的示例程序,确保其正常运行。后续在这个基础上做错误验证的捕获及备用UI展示的测试。

page.tsx

import App from "./App";

export default function Home() {
  return <App />;
}

components/App.tsx

import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";

import React, { ErrorInfo } from "react";

export default function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <Page3 />
    </>
  );
}

components/Page1.tsx

export default function Page1() {
  return (
    <div style={{ backgroundColor: "lightblue", paddingBottom: "20px" }}>
      <h3>Page1</h3>
    </div>
  );
}


components/Page2.tsx

export default function Page2() {
  return (
    <div style={{ backgroundColor: "yellow", paddingBottom: "20px" }}>
      <h3>Page2</h3>
    </div>
  );
}

components/Page3.tsx

import Page3Child from "./Page3Child";

export default function Page3() {
  return (
    <div style={{ backgroundColor: "grey", paddingBottom: "20px" }}>
      <h3>Page3</h3>
      <Page3Child />
    </div>
  );
}

components/Page3Child.tsx

export default function Page3Child() {
  const title = "Page3Child";
  return (
    <div style={{ backgroundColor: "orange", paddingBottom: "20px" }}>
      <h5>{title}</h5>
    </div>
  );
}

基本用法

首先,我们需要创建一个用于显示错误信息的组件。该组件将在接收错误信息后显示。

components/ErrorFallback.tsx

"use client";

import { FallbackProps } from "react-error-boundary";

export default function ErrorFallback({ error }: FallbackProps) {
  return (
    <div>
      <h2>发生错误</h2>
      <pre>{error.message}</pre>
    </div>
  );
}


接下来,我们将修改Page3Child组件的代码,使其故意引发组件渲染的错误。

components/Page3Child.tsx

import { JSX } from "react";

function ThrowError(): JSX.Element {
  throw new Error("Error in Page3Child");
}

export default function Page3Child() {
  const title = "Page3Child";

  return (
    <div style={{ backgroundColor: "orange", paddingBottom: "20px" }}>
      <h5>{title}</h5>
      <ThrowError />
    </div>
  );
}

现在,我们将在Page3组件中使用ErrorBoundary来包裹Page3Child组件,并指定FallbackComponent为我们刚刚创建的错误页面组件ErrorFallback.tsx。

components/Page3.tsx

import ErrorFallback from "./ErrorFallback";
import Page3Child from "./Page3Child";
import { ErrorBoundary } from "react-error-boundary";

export default function Page3() {
  return (
    <div style={{ backgroundColor: "grey", paddingBottom: "20px" }}>
      <h3>Page3</h3>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Page3Child />
      </ErrorBoundary>
    </div>
  );
}

此时,当Page3Child组件引发错误时,ErrorBoundary将捕获该错误并显示ErrorFallback中显示错误信息的组件。

修改ErrorBoundary的包裹范围

我们可以尝试修改ErrorBoundary的包裹范围。首先,我们将删除在Page3组件中使用的ErrorBoundary,并在App组件中包裹Page3组件。

components/App.tsx

"use client";

import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import ErrorBoundary from "@/components/ErrorBoundary";
import { Page4 } from "@/components/Page4";
import Page5 from "@/components/Page5";

export default function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <ErrorBoundary>
        <Page3 />
      </ErrorBoundary>
    </>
  );
}


此时,Page3部分将被完全替换为ErrorFallback中显示错误信息的组件。

接下来,我们将Page2Page3组件包裹在ErrorBoundary组件中。

components/App.tsx

import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './ErrorFallback';
import Page1 from './Page1';
import Page2 from './Page2';
import Page3 from './Page3';

function App() {
  return (
    <>
      <Page1 />
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Page2 />
        <Page3 />
      </ErrorBoundary>
    </>
  );
}
export default App;

此时,Page2Page3的内容将被完全替换为ErrorFallback中显示错误信息的组件。

最后,我们将所有组件都包裹在ErrorBoundary组件中。

components/App.tsx

import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@/components/ErrorFallback";

export default function App() {
  return (
    <>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Page1 />
        <Page2 />
        <Page3 />
      </ErrorBoundary>
    </>
  );
}

此时,整个页面将被完全替换为ErrorFallback中显示错误信息的组件。

使用resetErrorBoundary重新加载出现错误的组件

在发生错误时,我们可以在用于异常显示的ErrorFallback组件中添加一个重试按钮,当用户点击重置按钮时,调用resetErrorBoundary回调函数实现中的所有组件的重新加载。

components/ErrorFallback.tsx

"use client";
import { FallbackProps } from "react-error-boundary";

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div>
      <h2>发生错误</h2>
      <pre>{error.message}</pre>
      <button type="button" onClick={resetErrorBoundary}>
        再试一次
      </button>
    </div>
  );
}
export default ErrorFallback;


接下来,我们将Page3Child修改为高频率引发错误的状态,以便用户可以尝试重试失败和成功的效果。

components/Page3Child.tsx

"use client";
import React, { JSX } from "react";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";

function ThrowError(): JSX.Element {
  console.log("Page3Child 开始抛出错误");
  if (Math.random() < 0.5) {
    console.log("Page3Child 引发错误");
    throw new Error("Page3Child 引发错误");
  }
  return <p>未引发错误</p>;
}

function CustomeH5(): JSX.Element {
  console.log("加载CustomeH5");
  return <h5>Page3Child</h5>;
}

function Page3Child() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <div style={{ backgroundColor: "#DEB331" }}>
        <CustomeH5 />
        <ThrowError />
      </div>
    </ErrorBoundary>
  );
}
export default Page3Child;


点击再试一次的时候,包裹在ErrorBoundar里面的组件会重新加载。

捕获事件处理中的错误并重置

如果在点击事件中出现错误,可以被react-error-boundary捕获后跳转到错误页面,如果希望消除错误重新返回正常页面重试一次的情况下。 可以在捕获错误的页面中添加onReset方法,使用onReset可以指定重置错误状态的回调函数。 也可以指定resetKeys,通过监听resetKeys指定的数组中的数据,如果发生改变,也会重置错误更新画面。

components/Page3Child.tsx

"use client";
import React, { ErrorInfo } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";

function ThrowError(): React.JSX.Element {
  throw new Error("Page3Child 引发错误");
}

function onError(error: Error, info: ErrorInfo) {
  console.log("error.message", error.message);
  console.log("info.componentStack:", info.componentStack);
}

function Page3Child() {
  const [throwError, setThrowError] = React.useState(false);

  const onclick = () => {
    setThrowError((preThrowError) => !preThrowError);
  };

  const onReset = () => {
    setThrowError((prev) => !prev);
  };

  function CustomeH5(): React.JSX.Element {
    console.log("加载CustomeH5");
    return <h5>Page3Child</h5>;
  }

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={onError}
      onReset={onReset}
    >
      <div style={{ backgroundColor: "#DEB331" }}>
        <CustomeH5 />
        <button onClick={onclick}>抛出异常</button>
        {throwError && <ThrowError />}
      </div>
    </ErrorBoundary>
  );
}
export default Page3Child;



上面的代码块中,通过onReset={onReset}重新计算throwError的数据控制异常组建的显示或隐藏,通过点击再试一次的按钮可以再次执行onclick事件里面的方法。

JS事件处理程序中的错误

接下来,我们将介绍如何处理JS事件处理程序中的错误。我们将创建一个新的Page4组件,点击按钮时引发异常。

components/Page4.tsx

"use client";
import { useErrorBoundary } from "react-error-boundary";

function Page4() {
  const { showBoundary } = useErrorBoundary();

  const onClick = () => {
    try {
      throw new Error("Page4 引发错误");
    } catch (error: any) {
      showBoundary(error);
    }
  };

  return (
    <div style={{ backgroundColor: "#FF7272" }}>
      <h3>Page4</h3>
      <button type="button" onClick={onClick}>
        按钮
      </button>
    </div>
  );
}
export default Page4;


在事件处理程序中引发的异常将向上传播,直到被ErrorBoundary捕获。在这里,我们将Page4组件包裹在App组件的ErrorBoundary中。

components/App.tsx

"use client";
import ErrorFallback from "@/components/ErrorFallback";
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import Page4 from "@/components/Page4";

import React, { ErrorInfo } from "react";
import { ErrorBoundary } from "react-error-boundary";

const onError = (error: Error, info: ErrorInfo) => {
  console.log("错误信息:", error.message);
  console.log("组件栈:", info.componentStack);
};
export default function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
        <Page3 />
        <Page4 />
      </ErrorBoundary>
    </>
  );
}

处理异步操作中的错误

最后,我们将介绍如何处理异步操作中的错误。我们将创建一个新的Page5组件,点击按钮时进行异步操作以获取用户名。

components/Page5.tsx

import { useState } from "react";
import { useErrorBoundary } from "react-error-boundary";

const fetchUserAPI = async (): Promise<string> => {
  const username = "piyoko";
  const ss = new Date().getMilliseconds();
  if (ss % 2 === 0) {
    throw new Error("fetchUserAPI中的错误");
  }
  return username;
};

function Page5() {
  const [userName, setUserName] = useState("");
  const { showBoundary } = useErrorBoundary();

  const onClick = () => {
    fetchUserAPI()
      .then((res) => {
        setUserName(res);
      })
      .catch((err) => {
        showBoundary(err);
      });
  };

  return (
    <div style={{ backgroundColor: "#8CCDB0" }}>
      <h3>Page5</h3>
      <button type="button" onClick={onClick}>
        按钮
      </button>
      <p>用户名:{userName}</p>
    </div>
  );
}
export default Page5;

在异步操作中引发的错误将被ErrorBoundary捕获,直到被上层组件处理。

components/App.tsx

"use client";
import ErrorFallback from "@/components/ErrorFallback";
import Page1 from "@/components/Page1";
import Page2 from "@/components/Page2";
import Page3 from "@/components/Page3";
import Page4 from "@/components/Page4";
import Page5 from "@/components/Page5";

import React, { ErrorInfo } from "react";
import { ErrorBoundary } from "react-error-boundary";

const onError = (error: Error, info: ErrorInfo) => {
  console.log("错误信息:", error.message);
  console.log("组件栈:", info.componentStack);
};
export default function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
        <Page3 />
        <Page4 />
        <Page5 />
      </ErrorBoundary>
    </>
  );
}

总结

通过使用react-error-boundary,我们能够轻松处理组件中的错误、事件处理程序中的错误以及异步操作中的错误。这种方法不仅提高了代码的可维护性,还增强了用户体验,使得错误处理变得更加优雅和高效。


希望这篇文章能帮助你更好地理解如何在React应用程序中实现全面的错误处理机制!