用 Babel 给 React 组件起名字

背景

假设我们现在有这样一份代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyComp() {
return <span>My Comp</span>
}

function App(props) {
if (props.children.type.name.startsWith('My')) return props.children
}

ReactDOM.render(
<App>
<MyComp />
</App>,
document.getElementById('root')
)

这个代码的大概意思是在 App 组件中,只渲染组件名为 My 开头的子组件。但是当这份代码打包发布后,发现并没有生效,为什么呢?原因是打包过程中对代码进行了混淆,MyComp 已经被转为了其他的字符。这时,可以通过给组件添加一个变量来实现:

1
MyComp.displayName = 'MyComp'

App 组件中的判断逻辑也需要修改为:

1
if (props.children.type.displayName.startsWith('My'))

一个组件还好,如果项目中有成千上万个组件呢?所以,就衍生出了我们今天的主题:如何用 Babel 给 React 组件起名字?答案也很简单:写一个 Babel 插件。

Babel 插件

需求分析

首先,我们需要分析一下我们的 React 组件都有哪些可能的定义形式,以及他们在 Babel 中的节点类型。这里列举几个常见的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 1
function Comp() {
return <span>Comp</span>
}
/*
{
type: 'FunctionDeclaration',
id: { type: 'Identifier', name: 'Comp', loc: undefined },
}
*/


// 2
const Comp = () => {
return <span>Comp</span>
}
/*
{
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: [Object],
init: [Object],
loc: undefined
}
],
...
}
*/

// 3
export default () => {
return <span>Comp</span>
}
/*
{
type: 'ExportDefaultDeclaration',
declaration: {
type: 'ArrowFunctionExpression',
...
},
...
}
*/

// 4
class Comp extends React.Component {
render() {
return <span>Comp</span>
}
}
/*
{
type: 'ClassDeclaration',
id: { type: 'Identifier', name: 'Comp', loc: undefined },
body: { type: 'ClassBody', body: [ [Object] ], loc: undefined },
...
}
*/

// 5
export default class extends React.Component {
render() {
return <span>Comp</span>
}
}
/*
{
type: 'ExportDefaultDeclaration',
declaration: {
type: 'ClassDeclaration',
id: null,
body: { type: 'ClassBody', body: [Array], loc: undefined },
...
},
...
}
*/

这里还有一个问题是,如何区别普通函数/类与 React 的组件呢,答案就是看这些节点的子节点中是否含有 JSXElement 类型的节点。思路有了,代码就呼之欲出了。

插件实现

以下给出了两种情况的 visitor,其他的情况类似,就不赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function createDisplayNameNode(elementName, property = 'displayName') {
const node = t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(t.identifier(elementName), t.identifier(property)),
t.stringLiteral(elementName)
)
)
return node
}

function hasJSXElement(path) {
let hasJSXElement = false
path.traverse({
JSXElement(path) {
hasJSXElement = true
},
})
return hasJSXElement
}

function myCustomPlugin() {
return {
visitor: {
FunctionDeclaration(path) {
if (hasJSXElement(path)) {
path.insertAfter(createDisplayNameNode(path.node.id.name))
}
},
/**
* 处理有多个变量声明语句的情况:
* const C1 = () => { return <span>C1</span> }, C2 = () => { return <span>C2</span> }
*/
VariableDeclaration(path) {
const arr = []
path.traverse({
VariableDeclarator(path) {
if (hasJSXElement(path)) {
arr.push(path.node.id.name)
}
},
})
arr.forEach((name) => path.insertAfter(createDisplayNameNode(name)))
},
...
},
}
}

值得注意的是,上面的代码都是在 enter 阶段进行处理。其实放在 exit 阶段也可以,只不过需要注意与 @babel/plugin-transform-react-jsx 插件的顺序问题,必须在其前面,因为 @babel/plugin-transform-react-jsx 会在 exit JSXElement 的时候对 JSXElement 进行替换。

总结

本文从实际开发场景中引申出了如何给 React 组件添加名字的主题,并分析了 React 组件常见的几种定义方式,最后通过编写 Babel 插件实现了我们的需求。