React错误边界处理

2,546 阅读7分钟

大家好,我是梅利奥猪猪。今天和大家分享的主题是React错误边界处理,不知道大家有没有处理过React的错误边界?如果没有处理过,那就看我这篇文章,保姆级别手摸手指导!

案例

我们先写一个案例,用cra直接搭一个react项目!等依赖全部装完后yarn start启动下

react-start.gif

非常完美,那错误边界又是什么,会发生什么事情,很简单,我们来写anyscript,然后写错误的代码看下会怎么样,代码如下

import React from 'react';
import logo from './logo.svg';
import './App.css';

const MakeError = () => {
  const value: any = undefined; // 这行是新增的,any类型,赋值undefined
  return (
    // 我们对个undefined做了”.“操作,必然是会报错的
    <span>{value.notExist}</span>
  )
}

function App() {
  
  return (
    <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.tsx</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React 
            <MakeError/>
          </a>
        </header>
    </div>
  );
}

export default App;

然后此刻看下页面,看它会怎么发脾气

console-error.png

只见页面直接给你翻了个白眼,不带眼珠的这种(白屏模式),并且控制台给予了清晰的提示Cannot read properties of undefined (reading 'notExist'),这是显然的,这个错误不就是我们自己写的嘛(滑稽脸)

在实际工作中,或多或少可能就有这种出现错误直接白屏的情况,那我们应该怎么处理呢

错误边界 - React

白屏现象的原因

以下引用来自官方文档错误边界,复制黏贴白话解释组成了这篇水文(开心脸)

自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

我们对这一决定有过一些争论,但根据我们的经验,把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。

此变化意味着当你迁移到 React 16 时,你可能会发现一些已存在你应用中但未曾注意到的崩溃。增加错误边界能够让你在应用发生异常时提供更好的用户体验。

简单的说,就是我们没处理错误,它不开心把react的组件树全部写在,给你看不带眼珠的翻白眼,因为他们觉得错误的ui留在那比翻白眼更可怕!

所以要进入如何处理错误边界的关键环节了

错误边界概念

以下引用来自官方文档错误边界,复制黏贴白话解释组成了这篇水文(开心脸)

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

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

注意看,关键字是打印错误同时展示降级 UI

如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

注意看,只有class组件才可以,还要定义特殊的生命周期方法,官方文档后面有贴代码,当然这里我不会完全复制,会结合ts写一个简单的错误边界组件

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

上述代码也来自官方文档,简单的使用,Talk is cheap. Show me the code.,从代码上看,非常直观,如果没有错误,那我就展示MyWidget,有错误就展示降级UI并有打印的错误信息,接下来我们就实现个最简单的错误边界组件,拯救没有眼珠的翻白眼吧

实现错误边界

第一步

新建components文件夹,新建文件error-boundary.tsx,然后搭个类组件的架子(如果安装了vscode插件-ES7 React/Redux/GraphQL/React-Native snippets,直接rcc加tab就可以了,ps: rcc意思是react class component)

rcc.gif

别问我为什么不写函数组件,因为文档里也说了,错误边界必须用类组件

第二步

结合我们的ts,众所周知,我们Component的泛型是class Component<P, S>(不知道的小伙伴,可以鼠标点Component进去看),所以第一项指的就是我们的props,第二项指的就是我们的state

接下去我们就要考虑props要传什么,很简单,我们不是正常没有错误的时候展示children就可以了,错误的时候展示降级UI(显示错误的组件),然后我们的state只要记录error就可以了,先show大家code

import { Component, ReactElement, PropsWithChildren } from 'react'

// 错误时降级显示的UI组件,props就传个error就可以了,返回就是我们的jsx
type FallbackRender = (props: { error: Error | null }) => ReactElement

export default class ErrorBoundary extends Component<PropsWithChildren<{ fallbackRender: FallbackRender }>, { error: Error | null }> {
    state = {
        error: null, // 默认值 没有错误
    }
    render() {
        return (
            <div>

            </div>
        )
    }
}

这里需要解释的估计就是PropsWithChildren,我之前写过Utility Types系列水文PropsWithChildren其实就是Utility Type,我们看下他是怎么实现的

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

交叉类型,ts萌新可以简单理解,就相当于是Object.assign的操作,把你写的props和children合在了一起,前面也说了,我们的props,就只需要正确时渲染children,触发错误边界的时候渲染降级UI,用了这个PropsWithChildren会比较装逼!

第三步

实现静态方法static getDerivedStateFromError() ,这里就复制官方的代码改造下即可,第三步之后的代码如下

import { Component, ReactElement, PropsWithChildren } from 'react'

// 错误时降级显示的UI组件,props就传个error就可以了,返回就是我们的jsx
type FallbackRender = (props: { error: Error | null }) => ReactElement

export default class ErrorBoundary extends Component<PropsWithChildren<{ fallbackRender: FallbackRender }>, { error: Error | null }> {
    state = {
        error: null, // 默认值 没有错误
    }

    static getDerivedStateFromError(error: Error) {
        return { error };
    }

    render() {
        return (
            <div>

            </div>
        )
    }
}

第四步

到了封装这个组件的最后步了,处理render的逻辑

  • 没有error正常渲染children
  • 有error渲染降级UI 最终代码如下
import { Component, ReactElement, PropsWithChildren } from 'react'

// 错误时降级显示的UI组件,props就传个error就可以了,返回就是我们的jsx
type FallbackRender = (props: { error: Error | null }) => ReactElement

export default class ErrorBoundary extends Component<PropsWithChildren<{ fallbackRender: FallbackRender }>, { error: Error | null }> {
    state = {
        error: null, // 默认值 没有错误
    }

    static getDerivedStateFromError(error: Error) {
        return { error };
    }

    render() {
        const { error } = this.state
        const { children, fallbackRender } = this.props
        return error ? fallbackRender({ error }) : children
    }
}

第五步

那我们可以试下这个新写的组件啦,来到我们的App.tsx,撸码撸起

import React from 'react';
import logo from './logo.svg';
import './App.css';
import ErrorBoundary from './components/error-boundary';

const ErrorInfo = ({ error }: { error: Error | null }) => (
  <div>
    {error?.message}
  </div>
)

const MakeError = () => {
  const value: any = undefined; // 这行是新增的,any类型,赋值undefined
  return (
    // 我们对个undefined做了”.“操作,必然是会报错的
    <span>{value.notExist}</span>
  )
}

function App() {

  return (
    <div className="App">
      <ErrorBoundary fallbackRender={ErrorInfo}>
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.tsx</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
            <MakeError />
          </a>
        </header>
      </ErrorBoundary>
    </div>
  );
}

export default App;

此时在来看下页面,看下降级的UI是否渲染了

error- boundary.png

漂亮!非常成功,这个比没有眼珠的翻白眼好多了,至少用户知道有什么错误了!先不要急,我们还要试下没有错误的时候能不能正确渲染,我们把MakeError组件干掉在试试看

import React from 'react';
import logo from './logo.svg';
import './App.css';
import ErrorBoundary from './components/error-boundary';

const ErrorInfo = ({ error }: { error: Error | null }) => (
  <div>
    {error?.message}
  </div>
)

const MakeError = () => {
  const value: any = undefined; // 这行是新增的,any类型,赋值undefined
  return (
    // 我们对个undefined做了”.“操作,必然是会报错的
    <span>{value.notExist}</span>
  )
}

function App() {

  return (
    <div className="App">
      <ErrorBoundary fallbackRender={ErrorInfo}>
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.tsx</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
            {/* MakeError用来调试错误边界是否work */}
            {/* <MakeError /> */}
          </a>
        </header>
      </ErrorBoundary>
    </div>
  );
}

export default App;

work.png

大功告成,这样我们简单处理错误边界就搞定了

第三方插件

之前也说了,这篇文章只是带大家实现个基础的简单的错误边界。但这种这么基础的功能,怎么可能没有大佬造过轮子呢,请看react-error-boundary!这边教大家迅速看源码的小技巧,任何github的仓库,只要你在链接github.com中间加一个1s,比如github1s.com/bvaughn/rea…,就能用编辑器模式直接看源码。基础的思路是和我们差不多的,这边就不展开了,给大家一起学习的机会!

总结

今天的水文又写完了,总结就是,多看文档,是不是一下子就学会了新的知识!这里是梅利奥猪猪,希望能帮助到大家!

参考