前言
React项目常常会遇到整个页面突然白屏。遇到这种问题大概率是界面渲染过程中,组件render方法抛出异常导致React runtime崩溃。本文介绍编写webpack loader在项目构建中模块编译时自动对React组件做错误边界包裹处理。
阅读本文,你将会了解使用@babel/parser,@babel/traverse,@babel/template,@babel/generator四个库,把JavaScript模块代码,从源码按需转化成目标代码的实践过程。
为什么使用webpack loader
背景
在webpack模块编译阶段,入口bundle用到的模块都将逐一编译,每个模块的编译过程中会根据webpack的module配置,执行每个loader。
loader默认暴露的方法,能获取到模块的源码,经过处理并最终返回目标代码完成对一个模块的一次转译过程。
结论
可以使用loader,通过接收源码,分析源码,修改源码,最终返回源码的步骤实现对React组件模块的源码,增加包裹错误边界的逻辑。
步骤
1.分析源码
分析jsx/tsx模块的源码,有很多种方式。本方案主要使用@babel/parser对源码进行AST抽象语法树对象的转换。
源码转换成AST对象后,接下来要做的就是对AST对象的分析。本文主要介绍React的ESM规范模块做相关的处理,ESM规范下React的组件暴露主要有以下四种方式:
export default ComponentA // 情况1 export default
export default {ComponentA, ComponentB, ComponentC} // 情况2 export default {}
// 情况3 export const
export const ComponentA = (props) => {
// 组件代码实现
}
export {ComponentA, ComponentB, ComponentC} // 情况4 export {}
假定前置开发好错误边界组件,为高阶函数HOC:ErrorBoundaryWrap,目前需要做的是对以上四种方式的React模块export时,进行HOC的包裹,上文代码对应的转换如下:
export default ErrorBoundaryWrap(ComponentA) // 情况1
// 情况2
export default {
ComponentA: ErrorBoundaryWrap(ComponentA),
ComponentB: ErrorBoundaryWrap(ComponentB),
ComponentC: ErrorBoundaryWrap(ComponentC)
}
// 情况3
export const ComponentA = ErrorBoundaryWrap((props) => {
// 组件代码实现
})
// 情况4
const ComponentAerrorBoundary = ErrorBoundaryWrap(ComponentA)
const ComponentBerrorBoundary = ErrorBoundaryWrap(ComponentB)
const ComponentCerrorBoundary = ErrorBoundaryWrap(ComponentC)
export {
ComponentAerrorBoundary as ComponentA,
ComponentBerrorBoundary as ComponentB,
ComponentCerrorBoundary as ComponentC
}
2.修改源码
根据上述的四种情况,拟定一份源码,作为我们用例的编写:origin.jsx
import React from 'react'
const Hello1 = () => {
return (
<>
<h1>Hello1 is here</h1>
</>
)
}
const Hello2 = () => {
return (
<>
<h1>Hello2 is here</h1>
</>
)
}
export const Hello3 = () => {
return (
<>
<h1>Hello3 is here</h1>
</>
)
}
const Hello5 = () => {
return (
<>
<h1>Hello5 is here</h1>
</>
)
}
const Hello6 = () => {
return (
<>
<h1>Hello6 is here</h1>
</>
)
}
export {Hello1, Hello2}
export default {
Hello5, Hello6
}
// export default Hello1
或许这里有同学会有疑问,为什么不用tsx作为源码?
其实在使用@babel/parser把源码转换AST对象过程中,可以通过配置引入typescript插件,对tsx进行转译为js语法,所以在实际处理AST对象的过程中无需考虑typescript语法相关的节点。同理jsx插件会把源码中的jsx语法转换成React Element对象。
const sourceAst = parser.parse(source, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript']
})
AST对象分析
不了解AST相关类型对象同学,可以到https://astexplorer.net/或相同功能的网站,粘贴源码直观查看源码转换成AST对象后的结构。

接下来细说,上述四种情况中情况2具体处理流程:
export default {
Hello5, Hello6
}
转换为
export default {
Hello5: ErrorBoundaryWrap(Hello5),
Hello6: ErrorBoundaryWrap(Hello6),
}

通过工具或者对源码parse后sourceAst对象进行打印,可以看出export default是一个类型为ExportDefaultDeclaration的节点。
确定类型后,该怎么在sourceAst中寻找节点呢?接下来引出第二个库@babel/traverse。
traverse(sourceAst, {
Program(path){
// type 为'Program'节点处理
}
ImportDeclaration(path){
// type 为'ImportDeclaration'节点处理
}
ArrowFunctionExpression(path){
// type 为'ArrowFunctionExpression'节点处理
}
ExportSpecifier(path){
// type 为'ExportSpecifier'节点处理
}
ExportDefaultDeclaration(path){
// type 为'ExportDefaultDeclaration'节点处理
}
})
@babel/traverse默认返回traverse方法,traverse主要接收两个参数,AST对象,和针对各种类型节点的处理回调函数。
traverse方法会遍历传入的AST对象的每个节点,根据当前节点的类型执行对应的回调函数,回调函数会接收到path对象入参,path对象包含当前节点信息及该节点的父节点、子节点、兄弟节点相关对象的引用。
由于// export default Hello1被注释,当前AST中仅有一个类型为ExportDefaultDeclaration的节点,通过执行以下代码,能看到ExportDefaultDeclaration只会执行一次。
traverse(sourceAst, {
// 其他节点类型处理
ExportDefaultDeclaration(path){
console.log(`#ExportDefaultDeclaration`, path)
// type 为'ExportDefaultDeclaration'节点处理
}
})
// 输出一次 #ExportDefaultDeclaratio
完整代码请见 github.com/efoxTeam/re… 执行yarn && yarn test运行代码。
接下来分析上述代码打印出来的path对象:
// path对象的方法属性不完全展示
NodePath {
parentPath: <ref *1> NodePath { // 父节点path对象引用
},
node: Node { // 当前节点
type: 'ExportDefaultDeclaration', // 节点类型
declaration: Node { // 子节点类型 ObjectExpression 相对于代码 {Hello5, Hello6} 部分
type: 'ObjectExpression',
properties: [Array] // 子节点属性, 下文会再展开
}
},
type: 'ExportDefaultDeclaration', //当前节点类型
parent: Node { // 父节点
},
}
源AST转换为目标AST
得到要操作的节点对象,接下来再确认一下需要做的事情:
export default {
Hello5, Hello6
}
上述代码片段需要转换为下面的目标代码:
export default {
Hello5: ErrorBoundaryWrap(Hello5),
Hello6: ErrorBoundaryWrap(Hello6),
}
接下来打印ExportDefaultDeclaration类型的子节点properties属性值path.node.declaration.properties,对应代码{Hello5, Hello6}部分
console.log(path.node.declaration.properties)
// 保留关键部分的输出
ExportDefaultDeclaration [
{
type: 'ObjectProperty',
computed: false,
key: {
type: 'Identifier',
name: 'Hello5', // 能获取到Hello5 Key值
loc: undefined,
leadingComments: undefined,
innerComments: undefined,
trailingComments: undefined,
extra: {}
},
},
{
type: 'ObjectProperty',
computed: false,
key: {
type: 'Identifier',
name: 'Hello6', // 能获取到Hello6 Key值
loc: undefined,
leadingComments: undefined,
innerComments: undefined,
trailingComments: undefined,
extra: {}
},
}
]
拿到Hello5,Hello6两个Key值之后,可以构造出我们需要的代码片段,接下来介绍@babel/template库,使用templateAPI可以把源码转换为AST节点。生成新的AST节点后,可以对原AST进行节点插入或替换。代码如下:
let replaceNodeString = 'export default {'
let adot = ''
path.node.declaration.properties.forEach(item => {
if (item?.value?.name) {
replaceNodeString += ` ${adot} ${item.value.name}: ErrorBoundary(${item.value.name})`
adot = ','
}
})
replaceNodeString += '}'
const newNode = template.statement(replaceNodeString)()
newNode.isdeal = true
path.replaceWithMultiple([newNode])
上述代码通过遍历path.node.declaration.properties生成如下代码片段:
export default {
Hello5: ErrorBoundaryWrap(Hello5),
Hello6: ErrorBoundaryWrap(Hello6),
}
再把代码片段通过template.statement(replaceNodeString)()转换成目标节点对象,再使用path.replaceWithMultiple方法替换掉原本属于:
export default {
Hello5, Hello6
}
的节点。
以上就完成了对情况4的React暴露组件代码的错误边界包裹。另外三种情况的处理和避免重复处理相同的AST节点、误处理相同类型的AST节点、以及引入ErrorBoundaryWrap高阶组件的实现请见 github.com/efoxTeam/re…
3.生成目标代码
最后,当源AST经过处理,达到目标AST后,通过使用@babel/generator把目标AST转化为目标代码,作为loader露出方法的返回值返回。
const { code } = generate(sourceAst)
return code
到此,一个特定功能的模块转译loader完成。
结语
本方案主要通过webpack loader的实现,对jsx/tsx后缀的JavaScript模块,进行暴露引用的4种情况,进行错误边界的包裹。用工程化手段自动化对React组件做容错处理,避免组件渲染异常导致react runtime render的崩溃。
推荐相关读物
- babel 官网 babeljs.io/
- 掘金小册子《babel 插件通关秘籍》 juejin.cn/book/694611…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!