Errorboundary详解

背景

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

ErrorBoundary 组件主要用于 React 应用中的错误捕获,它可以捕捉 子组件树渲染期间生命周期方法 以及 组件树内部的事件处理 中发生的错误。

ErrorBoundary 可以捕捉的错误:

  • 渲染错误 例如 undefined.someMethod() 这种运行时错误。
  • 生命周期方法中的错误 componentDidMountcomponentDidUpdatecomponentWillUnmount 等生命周期方法中发生的错误。
  • setState 触发的错误 例如:this.setState(() => { throw new Error("Oops!"); })

ErrorBoundary 示例

笔者直接提供一个现成的 ErrorBoundary 组件示例代码,具体如下:

js 复制代码
import React, { 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.
    console.log("error from getDerivedStateFromError", error);
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    console.error("ErrorBoundary caught an error ------>", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

因为ErrorBoundary组件需要用到 getDerivedStateFromError 和 componentDidCatch 两个方法,而这两个重要方法只有类组件中有,所以 ErrorBoundary 组件只能是类组件

下面详细来看上面四种错误

渲染错误

渲染错误指React组件在渲染阶段发生的错误,该错误会被ErrorBoundary组件捕获,请看示例代码

js 复制代码
import "./App.css";

function App() {
  return (
    <>
      <RenderError />
    </>
  );
}

export default App;

// render error
function RenderError() {
  const a = null;

  a.foo(); // 运行时错误
   
  return (
    <div></div>
  );
}

上述代码出现 RenderError 组件渲染期间抛出运行时错误,会被ErrorBoundary组件捕获该错误,同时给出错误显示的自定义UI

生命周期方法中的错误

生命周期方法中的错误指在componentDidMountcomponentDidUpdatecomponentWillUnmount、useEffect等生命周期方法中发生的错误

下面是 useEffect 中抛出错误的示例代码

js 复制代码
import { useEffect } from "react";

import "./App.css";

function App() {
  return (
    <>
      <RenderError />
    </>
  );
}

export default App;

// render error
function RenderError() {
  const a = null;

  // a.foo();

  const handleClick = () => {
    a.foo();
  };

  useEffect(() => {
    a.foo();
  }, []);

  return (
    <div></div>
  );
}

在React函数组件中,用useEffect模拟mount生命周期钩子,抛出错误可以被

setState 触发的错误

setState 触发的错误也可以被ErrorBoundary 捕获,比如this.setState(() => { throw new Error("Oops!"); })

但是现在前端开发人员主要写React函数组件,setState属于React类组件语法,所以不用过于关注这种情况

前端导致的错误非常多,说完 ErrorBoundary 能够捕获的错误,下面看看 ErrorBoundary 不能捕捉的错误

ErrorBoundary 不能捕捉的错误

事件处理函数中的错误

大家一定遇到过事件处理函数中抛出的错误

js 复制代码
function RenderError() {
  const a = null;

  const handleClick = () => {
    a.foo();
  };

  return (
    <div>
      <button onClick={handleClick}>click</button>
    </div>
  );
}

ErrorBoundary 组件无法捕获这类错误,这类错误只会在console控制台输出错误,产品的界面上不会有任何变化

异步代码中的错误

js 复制代码
function RenderError() {
  const a = null;

  const handleClick = () => {
    setTimeout(() => {
      a.foo();
    });
  };

  return (
    <div>
      <button onClick={handleClick}>click</button>
    </div>
  );
}

既然事件处理函数中的错误和异步代码中的错误无法被ErrorBoundary组件捕获,但是有需要集中捕获这类错误,记录在日志中,方便定位问题和调试那怎么做呢?

因为React内部拦截事件冒泡,所以在React中的事件抛出的错误无法被捕获,那么可以对Hook一下 addEventListener 方法

js 复制代码
(function () {
  const originalAddEventListener = EventTarget.prototype.addEventListener;

  EventTarget.prototype.addEventListener = function (type, listener, options) {
    // console.log(`[Hook Event] 绑定事件: ${type}`, listener);

    const wrappedListener = function (...args) {
      try {
        return listener.apply(this, args);
      } catch (error) {
        console.error(`[Hook Event] 事件处理错误: ${type}`, error);
      }
    };

    return originalAddEventListener.call(this, type, wrappedListener, options);
  };
})();

然后在全局添加如下的代码

js 复制代码
window.addEventListener("error", (event) => {
  console.log('event', event);
});

window.addEventListener("unhandledrejection", (event) => {
  console.log('unhandledrejection error', event);
});

上述代码添加完成后,事件和异步代码中抛出的错误可以被正常捕获

服务端渲染(SSR)错误

ErrorBoundary 仅在客户端工作,无法在服务器端 React 代码(如 Next.js getServerSideProps)中捕捉错误

自身的错误

如果 ErrorBoundary 组件内部(而不是子组件)发生错误,它无法捕捉自身的错误。如果 ErrorBoundary 组件内部(而不是子组件)发生错误,它无法捕捉自身的错误。

这个问题是显而易见的

错误边界外部的错误

Reduxreducer 发生错误、全局 window 级别的 onerrorunhandledrejection 事件

最佳实践

在项目中添加ErrorBoundary.jsx,代码如下所示

js 复制代码
import React, { 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.
    console.log("error from getDerivedStateFromError", error);
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
  // 接入日志
    // You can also log the error to an error reporting service
    console.error("ErrorBoundary caught an error ------>", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

最后再添加 hook_addEventListener.js

js 复制代码
(function () {
  const originalAddEventListener = EventTarget.prototype.addEventListener;

  EventTarget.prototype.addEventListener = function (type, listener, options) {
    // console.log(`[Hook Event] 绑定事件: ${type}`, listener);

    const wrappedListener = function (...args) {
      try {
        return listener.apply(this, args);
      } catch (error) {
        console.error(`[Hook Event] 事件处理错误: ${type}`, error);
      }
    };

    return originalAddEventListener.call(this, type, wrappedListener, options);
  };
})();


window.addEventListener("error", (event) => {
   // 接入日志
  console.log('error', event);
});

window.addEventListener("unhandledrejection", (event) => {
// 接入日志
  console.log('unhandledrejection', event);
});
相关推荐
GIS之路10 分钟前
GeoTools 结合 OpenLayers 实现空间查询
前端
陳有味_ChenUvi11 分钟前
使用 pnpm 优雅搭建 Monorepo 仓库
前端·npm·前端工程化
旷世奇才李先生13 分钟前
XML DOM 安装使用教程
xml·前端·chrome
程序员秘密基地20 分钟前
基于html,css,vue,vscode,vs2022,asp.net,aspnet,.net,c#,mysql数据库,在线健身,俱乐部管理系统
前端·vue.js·后端·mysql·asp.net
Mintopia22 分钟前
Three.js 画布纹理:像素世界的魔法编织术
前端·javascript·three.js
天天摸鱼的java工程师23 分钟前
当我成为面试官,我才知道当年那些面试官其实并不是在难为我,而是在考察我面对问题的拆解能力
前端·后端·面试
袁煦丞26 分钟前
泰拉瑞亚远程联机魔法:cpolar内网穿透实验室第617个成功挑战
前端·程序员·远程工作
玲小珑30 分钟前
Next.js 教程系列(十二)API Routes:构建轻量级后端服务
前端·next.js
JinSo35 分钟前
EasyEditor AI 聊天助手:让低代码开发更简单
前端·javascript·github
答案answer39 分钟前
three.js 实现几个炫酷的粒子特效(火焰,烟雾,烟花)
前端·three.js