实现一个自动注入 ErrorBoundary 的 Babel 插件
React 的 ErrorBoundary 很好用,但每个组件都手动包裹太繁琐。本文实现一个 Babel 插件,自动为组件注入错误边界。
痛点
React 16 引入了 ErrorBoundary,但使用起来很麻烦:
// 每个可能出错的组件都要手动包裹
<ErrorBoundary>
<UserAvatar />
</ErrorBoundary>
<ErrorBoundary>
<UserBio />
</ErrorBoundary>
<ErrorBoundary>
<UserSettings />
</ErrorBoundary>
能不能自动化?
设计思路
通过 Babel 插件在编译时自动转换:
// 转换前
<UserBio />
// 转换后
const _ERROR_HOC_UserBio = withErrorHandler(UserBio);
<_ERROR_HOC_UserBio isCatchReactError />
核心步骤:
- 遍历 AST,找到所有 JSX 元素
- 过滤掉原生标签(div、span 等)
- 为自定义组件创建 HOC 包装
- 替换原 JSX 元素
实现
插件结构
module.exports = function ({ types: t }, config) {
return {
visitor: {
Program(path) {
// 在文件开头插入 import 语句
},
ReturnStatement(path) {
// 遍历 return 中的 JSX
path.traverse(customComponentVisitor);
},
},
};
};
核心逻辑
const customComponentVisitor = {
JSXElement(path) {
const jsxName = path.node.openingElement.name.name;
// 1. 过滤原生标签
if (htmlTags.includes(jsxName)) return;
// 2. 过滤已转换的组件
if (jsxName.includes("_ERROR_HOC_")) return;
// 3. 创建 HOC
const HOC_NAME = createHoc(path, jsxName);
// 4. 构建新的 JSX 元素
const newJsx = t.JSXElement(
t.JSXOpeningElement(t.JSXIdentifier(HOC_NAME), [
t.jsxAttribute(t.jsxIdentifier("isCatchReactError")),
...path.node.openingElement.attributes,
]),
t.JSXClosingElement(t.JSXIdentifier(HOC_NAME)),
path.node.children
);
// 5. 替换
path.replaceWith(newJsx);
},
};
创建 HOC
const createHoc = (path, tagName) => {
const hocName = `_ERROR_HOC_${tagName}`;
// 找到组件所在作用域
const scope = findJSXElementScope(path.scope, tagName);
// 检查是否已转换
if (scope.path.__transformInfo?.includes(tagName)) {
return hocName;
}
// 生成 HOC 声明
const hocNode = template.ast(
`const ${hocName} = withErrorHandler(${tagName})`
);
// 插入到 return 语句前
const body = scope.path.node.body;
if (body.type === "BlockStatement") {
body.body.splice(body.body.length - 1, 0, hocNode);
}
// 记录已转换
scope.path.__transformInfo = scope.path.__transformInfo || [];
scope.path.__transformInfo.push(tagName);
return hocName;
};
作用域查找
const findJSXElementScope = (scope, variableName) => {
if (scope.hasOwnBinding(variableName)) {
return scope;
}
if (scope.parent) {
return findJSXElementScope(scope.parent, variableName);
}
return null;
};
配置使用
webpack 配置
// webpack.config.js
{
test: /\.(js|jsx|ts|tsx)$/,
loader: "babel-loader",
options: {
plugins: [
[
require.resolve("./config/plugins/auto-log-plugin"),
{
imports: `import withErrorHandler from "@/components/hoc/withErrorHandler"`,
errorHandleComponent: "withErrorHandler",
ignore: ["WrappedComponent", "ReactErrorBoundary"],
risks: "all",
},
],
],
},
}
配置项说明
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
imports | string | - | HOC 的 import 语句 |
errorHandleComponent | string | - | HOC 函数名 |
ignore | string[] | [] | 忽略的组件名列表 |
risks | "all" | string[] | "all" | 需要包裹的组件,"all" 表示全部 |
HOC 实现
// withErrorHandler.js
import { ErrorBoundary } from "react-error-boundary";
const ErrorFallback = ({ error, resetErrorBoundary }) => (
<div role="alert">
<h2>出错了</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
export default (WrappedComponent) => (props) => (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error) => console.error("Error:", error)}
>
<WrappedComponent {...props} />
</ErrorBoundary>
);
转换示例
转换前
function UserInfo() {
return (
<div>
<UserAvatar />
<UserBio />
</div>
);
}
转换后
import withErrorHandler from "@/components/hoc/withErrorHandler";
function UserInfo() {
const _ERROR_HOC_UserAvatar = withErrorHandler(UserAvatar);
const _ERROR_HOC_UserBio = withErrorHandler(UserBio);
return (
<div>
<_ERROR_HOC_UserAvatar isCatchReactError />
<_ERROR_HOC_UserBio isCatchReactError />
</div>
);
}
测试效果
创建一个会随机抛错的组件:
// useErrorSimulator.js
const useErrorSimulator = ({ probability = 0.5, message = "Error" }) => {
useEffect(() => {
if (Math.random() < probability) {
throw new Error(message);
}
}, []);
};
// UserBio.js
const UserBio = () => {
useErrorSimulator({ probability: 0.5 });
return <p>User biography...</p>;
};
运行后,UserBio 出错时会显示 ErrorFallback,而不会导致整个页面崩溃。
局限性
- 只处理渲染时错误:事件处理器中的错误需要 try-catch
- 性能开销:每个组件都包裹 HOC 会增加组件层级
- 调试困难:组件名变成
_ERROR_HOC_xxx,React DevTools 中不太直观
优化方向
- 按需包裹:通过
risks配置只包裹高风险组件 - 生产环境关闭:开发环境启用,生产环境禁用
- 错误上报:集成 Sentry 等监控服务
总结
这个 Babel 插件展示了 AST 转换的强大能力。核心思路:
- 遍历 JSX 元素
- 过滤原生标签
- 创建 HOC 包装
- 替换原元素
完整代码见 react-debug/error_boundary 分支。
📦 项目地址:github.com/220529/reac…(error_boundary 分支)
系列文章:React 源码调试环境搭建