背景
ErrorBoundary
是一种 React
组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript
错误,并打印这些错误,同时展示降级UI,而并不会渲染那些发生崩溃的子组件树。 错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
ErrorBoundary
组件主要用于 React 应用中的错误捕获,它可以捕捉 子组件树 在 渲染期间 、生命周期方法 以及 组件树内部的事件处理 中发生的错误。
ErrorBoundary
可以捕捉的错误:
- 渲染错误 例如
undefined.someMethod()
这种运行时错误。 - 生命周期方法中的错误
componentDidMount
、componentDidUpdate
、componentWillUnmount
等生命周期方法中发生的错误。 - 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
生命周期方法中的错误
生命周期方法中的错误指在componentDidMount
、componentDidUpdate
、componentWillUnmount
、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
组件内部(而不是子组件)发生错误,它无法捕捉自身的错误。
这个问题是显而易见的
错误边界外部的错误
Redux
的 reducer
发生错误、全局 window
级别的 onerror
或 unhandledrejection
事件
最佳实践
在项目中添加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);
});