在一个月黑风高的晚上,小黄问我 babel
你知道么?
我: 我靠,babel
我肯定知道啊,你这是在侮辱谁?
小黄: 那你知道 babel
是怎么工作的么?
我: 当然了,第一步是进行词法、语法解析,解析成 AST,第二步就是接收到 AST 之后,对节点进行添加、更新、删除等操作,这也是插件要介入的地方,第三步就是把转换后的 AST 转换成字符串的代码。
小黄: 厉害啊,那你写过插件么?
我: 没有,没写过。
小黄: 你居然没写过?
我: 我靠,你什么眼神?等着,我晚上给你撸一个,对了,你有资料吗?
小黄: 有,拿着 Babel 插件手册
要干个啥?
说了要写一个插件,总不能写个 1 + 1 等于 2 吧。正好在看 antd
的内容,那就搞个简单的按需加载的插件吧。
// 我们这么写
import { Button, Table } from 'antd';
// 其实相当于
import Button from 'antd/lib/button';
import Table from 'antd/lib/table';
所以,目标确定了,我们还等啥呢?
撸起袖子开干
在大概的了解了上面的资料之后,我们就开干了。
通过阅读资料,我们知道 Babel 插件需要我们暴露一个方法,这个方法返回一个含有 visitor
的对象。
module.exports = ({ types: t }) => {
visitor: {
}
};
那么我们要怎么去分析代码呢?很简单,我们去把代码贴到 AST explorer 里面,然后把转换的结构就可以拿出来分析了。
通过上面的分析,我们可以知道 import { Button, Table } from 'antd';
是一个 ImportDeclaration
节点,所以我们需要去操作这个类型的节点,所以在 visitor 里我们这么写。
module.exports = ({ types: t }) => ({
visitor: {
ImportDeclaration(path) {
// 我们可以通过 specifiers 拿到导入的东西,source 来拿到从哪个库导入的
const { specifiers, source } = path.node;
// 排除 default 的导入情况
if (!t.isImportDefaultSpecifier(specifiers[0])) {
}
}
}
});
上面使用的 babel.types
是一个帮助大家更好的操作节点的工具库,大家可以到这里去看详细文档。
好了,上面我们已经拿到了导入的语句,现在我们去拿到导入了什么东西,以及从什么库导入的,这样我们就能够完成转换了。
module.exports = ({ types: t }) => ({
visitor: {
// 我们可以通过第二个参数里的 opts 来拿到参数配置
ImportDeclaration(path, { opts }) {
// 我们可以通过 specifiers 拿到导入的东西,source 来拿到从哪个库导入的
const { specifiers, source } = path.node;
// 排除 default 的导入情况
if (!t.isImportDefaultSpecifier(specifiers[0])) {
const declarations = specifiers.map(specifier => {
// 我们可以通过 specifier.imported.name 获取到导入的东西
// 先写死 antd
const importPath = `${source.value}/lib/${specifier.imported.name}`;
// 我们通过上面的信息返回了一条 import 语句
return t.ImportDeclaration(
[t.ImportDefaultSpecifier(specifier.local)],
t.StringLiteral(importPath)
);
});
// 将上面生成的 import 语句替换到原来的语句
path.replaceWithMultiple(declarations);
}
}
}
});
大功告成,那么编写一个 babel 插件的大概流程就是这样,当然了,这个简单的插件肯定还有很多配置、边界问题等等没有考虑到,这个简单的 demo 我上传到了我的仓库。