Babel-插件

前言

一个最基本的插件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 新建一个 @babel/xx-xx 的包,根目录新建 index.js,发包的时候 npm publish

// 接受一个 babel-core 对象
export default function(babel) {
const {types: t} = babel
return {
pre(state) {
// 前置操作,可选,可以用于准备一些资源
},
visitor: {
// 我们的访问者代码将放在这里
ImportDeclaration(path, state) { // state 可以获取用户传入的参数
// ...
}
},
post(state) {
// 后置操作,可选
}
}
}

例子

最简单的例子

foo === bar; 换成 sebmck === dork;

AST Explorer

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
// foo === bar; 的 AST 如下
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}

export default function({ types: t }) {
return {
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}

path.node.left = t.identifier("sebmck");
path.node.right = t.identifier("dork");
}
}
};
}

极简版babel-plugin-import

实现模块的按需导入

img
1
2
3
4
5
6
7
8
import {A, B, C as D} from 'foo'
// 转换成
import A from 'foo/A'
import 'foo/A/style.css'
import B from 'foo/B'
import 'foo/B/style.css'
import D from 'foo/C'
import 'foo/C/style.css'

通过上面展示的结果,我们需要处理 ImportDeclaration 节点类型,将它的specifiers拿出来遍历处理一下。另外如果用户使用了默认导入语句,我们将抛出错误,提醒用户不能使用默认导入.

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
// 用户配置
{
plugins: [['toy-plugin', {name: 'foo'}]]
}

export default function({ types: t }) {
return {
visitor: {
// 访问导入语句
ImportDeclaration(path, state) {
const mod = state.opts && state.opts.name
if (mod == null || path.node.source.value !== mod) {
return
}

// 如果是空导入则直接删除掉
const specs = path.node.specifiers
if (specs.length === 0) {
path.remove()
return
}

// 判断是否包含了默认导入和命名空间导入
if (specs.some(i => t.isImportDefaultSpecifier(i) || t.isImportNamespaceSpecifier(i))) {
// 抛出错误,Babel会展示出错的代码帧
throw path.buildCodeFrameError("不能使用默认导入或命名空间导入")
}

// 转换命名导入
const imports = []
for (const spec of specs) {
const named = MODULE + '/' + spec.imported.name
const local = spec.local
imports.push(t.importDeclaration([t.importDefaultSpecifier(local)], t.stringLiteral(named)))
imports.push(t.importDeclaration([], t.stringLiteral(`${named}/style.css`)))
}

// 替换原有的导入语句
path.replaceWithMultiple(imports)
}
}
};
}

参考文章

深入浅出 Babel 上篇:架构和原理 + 实战

更多的插件操作,如获取子节点path等,可继续阅读:Babel 插件手册

仓库