如何处理React中的错误(超详细指南)

107 阅读8分钟

让我们面对它。没有人愿意在网上冲浪时看到一个破碎的、空的页面。它让你搁浅和迷惑。你不知道发生了什么,也不知道是什么原因造成的,让你对网站留下了不好的印象。

沟通错误并让用户继续使用该应用程序通常是更好的做法。用户会得到较少的坏印象,可以继续使用其功能。

在今天的文章中,我们将通过不同的方式来处理React应用程序中的错误。

React中经典的 "Try and Catch "方法

如果你使用过JavaScript,你可能不得不写一个 "try and catch "语句。为了确保我们了解它是什么,这里有一个:

try {
  somethingBadMightHappen()
} catch (error) {
  console.error("Something bad happened")
  console.error(error)
}

它是一个很好的工具,可以捕捉行为不端的代码,确保我们的应用程序不会被炸成碎片。为了更现实,尽可能接近React世界,让我们看一个例子,看看你如何在你的应用程序中使用这个:

const fetchData = async () => {
  try {
    return await fetch("https://some-url-that-might-fail.com")
  } catch (error) {
    console.error(error) // You might send an exception to your error tracker like AppSignal
    return error
  }
}

在React中进行网络调用时,你通常会使用try...catch 语句。但为什么呢?不幸的是,try...catch only 在命令式代码上工作。它不适用于声明性代码,比如我们在组件中编写的JSX。所以这就是为什么你没有看到一个巨大的try...catch 来包装我们的整个应用程序。它就是行不通的。

那么,我们该怎么做?很高兴你这么问。在React 16中,引入了一个新的概念--React错误边界。让我们来探讨一下它们是什么。

React的错误边界

在我们进入错误边界之前,让我们先看看为什么它们是必要的。想象一下,你有一个这样的组件:

const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>
}

export default CrashableComponent

如果你试图在某个地方渲染这个组件,你会得到一个像这样的错误。

不仅如此,整个页面将是空白的,用户将无法做或看到任何东西。但是发生了什么?我们试图访问一个属性iDontExist.prop ,而这个属性并不存在(我们没有把它传给这个组件)。这是个平庸的例子,但它表明我们不能用try...catch 语句来捕捉这些错误。

这整个实验把我们带到了错误边界。错误边界是React组件,它可以在其子组件树的任何地方捕获JavaScript错误。然后,它们会记录这些捕捉到的错误,并显示一个后备UI,而不是崩溃的组件树。错误边界在渲染过程中,在生命周期方法中,以及在它们下面的整个树的构造函数中捕捉错误。

错误边界是一个类组件,它定义了生命周期方法static getDerivedStateFromError()componentDidCatch() 中的一个(或两个)。static getDerivedStateFromError() 在抛出错误后渲染一个回退用户界面。componentDidCatch() 可以将错误信息记录到你的服务提供者(如AppSignal)或浏览器控制台中。

让我们看看一个典型的错误边界组件:

import { Component } from "react"

class ErrorBoundary extends 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,
      error,
    }
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    const { hasError, error } = this.state

    if (hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          <p>Something went wrong 😭</p>

          {error.message && <span>Here's the error: {error.message}</span>}
        </div>
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary

我们可以像这样使用ErrorBoundary:

<ErrorBoundary>
  <CrashableComponent />
</ErrorBoundary>

现在,当我们打开我们的应用程序时,我们将得到一个工作的应用程序,内容如下:

这正是我们想要的。我们希望我们的应用程序在发生错误时仍能保持功能。但我们也希望将错误告知用户(和我们的错误跟踪服务)。

请注意,使用错误边界并不是一个银弹。错误边界并不能捕捉到以下的错误:

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

你仍然需要对这些家伙使用try...catch 语句。所以,让我们继续展示你如何做到这一点。

事件处理程序中的错误捕获

如前所述,当错误在事件处理程序中被抛出时,错误边界不能帮助我们。让我们看看我们如何处理这些。下面是一个小的按钮组件,当你点击它时抛出一个错误:

import { useState } from "react"

const CrashableButton = () => {
  const [error, setError] = useState(null)

  const handleClick = () => {
    try {
      throw Error("Oh no :(")
    } catch (error) {
      setError(error)
    }
  }

  if (error) {
    return <span>Caught an error.</span>
  }

  return <button onClick={handleClick}>Click Me To Throw Error</button>
}

export default CrashableButton

请注意,我们在handleClick 里面有一个try和catch块,确保我们的错误被捕获。如果你渲染这个组件并试图点击它,就会发生这种情况。

我们在其他情况下也要这样做,比如在setTimeout

setTimeout 调用中的错误捕获

想象一下,我们有一个类似的按钮组件,但这个组件在被点击时调用setTimeout 。下面是它的样子:

import { useState } from "react"

const SetTimeoutButton = () => {
  const [error, setError] = useState(null)

  const handleClick = () => {
    setTimeout(() => {
      try {
        throw Error("Oh no, an error :(")
      } catch (error) {
        setError(error)
      }
    }, 1000)
  }

  if (error) {
    return <span>Caught a delayed error.</span>
  }

  return (
    <button onClick={handleClick}>Click Me To Throw a Delayed Error</button>
  )
}

export default SetTimeoutButton

在1000毫秒之后,setTimeout 回调将抛出一个错误。幸运的是,我们把这个回调逻辑包在try...catch ,和setError 组件中。这样一来,在浏览器控制台中就不会显示堆栈跟踪。此外,我们还将错误传达给用户。下面是它在应用程序中的样子。

这一切都很好,因为尽管后台到处都是错误,我们还是让我们的应用程序的页面运行起来了。但是,有没有一种更简单的方法来处理错误,而不需要编写自定义错误边界?你肯定有,当然,它是以JavaScript包的形式出现的。让我向你介绍一下react-error-boundary

JavaScript的react-error-boundary

你可以在你的package.json ,比以前更快地弹出该库:

npm install --save react-error-boundary

现在,你已经准备好使用它了。还记得我们做的ErrorBoundary 组件吗?你可以忘了它,因为这个包导出了它自己的。下面是如何使用它:

import { ErrorBoundary } from "react-error-boundary"
import CrashableComponent from "./CrashableComponent"

const FancyDependencyErrorHandling = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error) => {
        // You can also log the error to an error reporting service like AppSignal
        // logErrorToMyService(error, errorInfo);
        console.error(error)
      }}
    >
      <CrashableComponent />
    </ErrorBoundary>
  )
}

const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong 😭</p>

    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
)

export default FancyDependencyErrorHandling

在上面的例子中,我们渲染了同样的CrashableComponent ,但这次我们使用了来自 react-error-boundary 库的ErrorBoundary 组件。它和我们的自定义组件做了同样的事情,只是它接收了FallbackComponent 道具和onError 函数处理器。其结果与我们自定义的ErrorBoundary 组件相同,只是你不必担心维护它,因为你使用的是一个外部包。

这个包的一个好处是,你可以很容易地把你的函数组件包装成一个withErrorBoundary ,使其成为一个高阶组件(HOC)。下面是它的样子:

import { withErrorBoundary } from "react-error-boundary"

const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>
}

export default withErrorBoundary(CrashableComponent, {
  FallbackComponent: () => <span>Oh no :(</span>,
})

很好,你现在可以去捕捉所有那些困扰你的错误了。

但是,也许你不希望在你的项目中出现另一个依赖关系。你能自己实现它吗?当然可以。让我们来看看如何做到这一点。

使用你的边界

你可以实现类似的,甚至相同的效果,你从react-error-boundary 。我们已经展示了一个自定义的ErrorBoundary 组件,但让我们改进它:

import { Component } from "react"

export default class ErrorBoundary extends 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,
      error,
    }
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }

  render() {
    const { hasError, error } = this.state

    if (hasError) {
      // You can render any custom fallback UI
      return <ErrorFallback error={error} />
    }

    return this.props.children
  }
}

const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong 😭</p>

    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
)

const errorBoundary = (WrappedComponent) => {
  return class extends ErrorBoundary {
    render() {
      const { hasError, error } = this.state

      if (hasError) {
        // You can render any custom fallback UI
        return <ErrorFallback error={error} />
      }

      return <WrappedComponent {...this.props} />
    }
  }
}

export { errorBoundary }

现在你得到了ErrorBoundary 和HOCerrorBoundary ,你可以在你的应用程序中使用。尽情地扩展和玩弄它吧。你可以让它们接收自定义的回退组件,以自定义你如何从每个错误中恢复。你也可以让它们接收一个onError 道具,然后在componentDidCatch 里面调用它。这种可能性是无穷无尽的。

但有一件事是肯定的--你毕竟不需要那个依赖性。我敢打赌,编写你自己的错误边界会带来成就感,而且你会更加了解它。另外,谁知道当你试图定制它时,你会得到什么想法。

总结一下:开始使用React错误处理

谢谢你阅读这篇关于在React中处理错误的博文。我希望你在阅读和尝试的过程中能像我写这篇文章一样开心。你可以在我创建的GitHub repo中找到所有的代码和例子。

简单介绍一下我们所经历的事情:

  • React错误边界对于捕捉声明性代码中的错误非常有用(例如,在其子组件树内)。
  • 对于其他情况,你需要使用try...catch 语句(例如,像setTimeout ,事件处理程序,服务器端渲染,以及在错误边界本身抛出的错误)。
  • react-error-boundary 这样的库可以帮助你写更少的代码。
  • 你也可以运行你自己的错误边界,并根据你的需要定制它。

这就是全部,伙计们。谢谢大家的收听,下一讲再见