前言
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
库,使用template
API可以把源码转换为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介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!