Babel有3个重要的步骤:解析(parse), 转换(transform), 生成(generate). 其中解析是把代码转成AST,所以咱们要先了解AST.
AST入门
定义:抽象语法树或者称为语法树,是用抽象语句的树结构来描述编程语言的源代码。树中的每个节点代表了源码中出现的一个结构。--维基百科
AST explorer是一个很棒的转换显示AST的在线网站,下面的例子都在这里运行
function add(a,b){
return a+b
}
上面的代码可以转换成下面的树结构:
{
"type": "Program",
"start": 0,
"end": 214,
"body": [
{
"type": "FunctionDeclaration",
"start": 179,
"end": 214,
"id": {
"type": "Identifier",
"start": 188,
"end": 191,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 192,
"end": 193,
"name": "a"
},
{
"type": "Identifier",
"start": 194,
"end": 195,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 196,
"end": 214,
"body": [
{
"type": "ReturnStatement",
"start": 202,
"end": 212,
"argument": {
"type": "BinaryExpression",
"start": 209,
"end": 212,
"left": {
"type": "Identifier",
"start": 209,
"end": 210,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 211,
"end": 212,
"name": "b"
}
}
}
]
}
}
],
"sourceType": "module"
}
虽然上面的JSON结构很长,但是把每个结构折叠起来看,就会发现基本上都是类似的。
{
"type":"FunctionDeclaration",
"start": 179,
"end": 214,
"id":{...}
}
{
"type":"BinaryExpression",
"start": 209,
"end": 212,
"left":{...},
"right":{...}
}
像上面的每一层结构也称为节点
, 一个AST树可以由成百上千个节点构成。如果用接口(Interface)来表示,如下所示:
interface Node {
type: string;
...
}
type
代表节点的类型,常见的有:
key | value |
---|---|
FunctionDeclaration | 函数声明 |
Identifier | 标识符 |
BinaryExpression | 二进制表达式 |
除了type,通用的属性还有start
,end
,loc
。当然不同的type
类型会附带不同的辅助属性来描述节点信息。
开始编写插件
- 第一步要先在
.babelrc
添加自定义的插件
{
...
"plugins": ["./babel/index"] //你的插件目录位置
}
- 新建插件文件,把下面的模板copy进去
module.exports = function ({ types: t }) {
return {
visitor: {
}
};
};
解析:函数会接收当前的babel
作为入参,由于会经常用到babel.types
,所以可以通过对象解构来获取types({types:t})
.在返回的对象中,visitor
是这个插件的主要访问者,visitor里面有很多函数,每个函数都会接收两个参数path
,state
。
下面通过一些简单的使用场景来学习编写插件
场景一:去掉console
假设需要在代码中去掉所有的console打印,首先需要了解console的AST结构,可以到AST explorer查看,下面是截图:
在例子中可以看到离console.log
的最近父节点是MemberExpression
,那么就可以在visitor
对象里面编写这个函数,如下所示:
visitor:{
MemberExpression(path){
...
}
}
接下来就要判断里面的语句是否包含console,如果没有,需要提前返回,如何判断呢?有两种方法,一种是直接在path
对象中查找,另一种是使用babel自带的方法查找:
//方法一
if(path.node.object.name!=='console') return
//方法二
if (!path.get('object').isIdentifier({ name: 'console' })) return;
找到console语句后,接着就需要移除该语句了,首先移除节点可以使用path.remove
方法,但是这里直接使用会报错,我们需要找到属于整条语句的节点,仔细看上面的截图,发现像console.log
这种表达式是属于ExpressionStatement
,可以通过findParent
方法查找父节点,从而删除整个语句。
// 向上查找父节点并删除
const parent = path.findParent((path) => path.isExpressionStatement());
parent.remove();
整体代码如下所示:
module.exports = function ({ types: t }) {
return {
visitor: {
MemberExpression(path) {
if (!path.get('object').isIdentifier({ name: 'console' })) return;
const parent = path.findParent((path) => path.isExpressionStatement());
parent.remove();
}
}
};
};
场景二:替换敏感词
假设有个需求要替换代码中的敏感词,比如下面的代码:
const output = '你居然想白嫖,还想下次一定???';
console.log(output)
首先第一步还是要拿到对应的AST结构,如下图所示,output
语句最近的父级节点是VariableDeclarator
第二步定义一个黑名单列表,这里选择从插件配置中传入,只需要修改.babelrc
文件:
"plugins": [
["./babel/index",{
"blackList":["下次一定","白嫖"]
}]
]
接着在代码中,通过state
来获取传入的参数:
VariableDeclarator(path,state){
const {blackList} = state.opts;
...
}
最后就简单了,用正则替换对应的字符串即可,整体代码如下:
VariableDeclarator(path,state){
if(!path.node.init) return;
let value = path.node.init.value;
const {blackList} = state.opts;
if(!blackList) return
blackList.forEach(item=>{
value = value.replace(new RegExp(item,'ig'),'**');
})
path.node.init.value = value;
}
场景三:箭头函数转换
下面我们来看看如何把箭头函数转成普通的函数。下面是测试示例:
//简单的箭头函数
const fn = (data)=>{
console.log(data)
}
其实有个自带的方法arrowFunctionToExpression
可以直接转换,看代码:
ArrowFunctionExpression(path){
if (!path.isArrowFunctionExpression()) return;
path.arrowFunctionToExpression()
}
嗯,有点简单,感觉不过瘾,要不咱们来手动转换一下。来一个简单的版本吧,先捋一下思路:
获取箭头函数的结构,然后生成一个函数结构替换
。
生成函数结构需要用到functionDeclaration
方法,该方法一共可以传5个参数,这里重点讲一下前3个参数:第一个是标识id
;第二个是参数数组
;第三个是主体body
,下面一步步来获取所需要的参数
- 标识id
// 标识id可以通过父级直接获取id属性
const name = path.parent.id&&path.parent.id.name? path.parent.id.name:'_fn'
- 参数数组
//参数可以通过params属性获取,注意获取到的是一个数组,需要遍历取node
const paramsSource = path.get('params')
const params = []
for(let i=0;i<paramsSource.length;i++){
params.push(paramsSource[i].node)
}
- 主体body
//主体可以直接拿箭头函数的body来copy
const body = path.get('body').node
整体代码如下:
ArrowFunctionExpression(path){
if (!path.isArrowFunctionExpression()) return;
const name = path.parent.id&&path.parent.id.name? path.parent.id.name:'_fn';
const paramsSource = path.get('params');
const params = [];
for(let i=0;i<paramsSource.length;i++){
params.push(paramsSource[i].node)
}
const node = t.functionDeclaration(t.Identifier(name), params, path.get('body').node);
const parent = path.findParent((path)=>path.isVariableDeclaration());
parent.replaceWith(node); //替换箭头函数结构体
}
上面写的转换代码还是比较粗糙的,还有一些地方没有考虑到,比如this指向的问题,如果想深入理解,可以看arrowFunctionToExpression的源码
总结
通过上面几个场景,对babel插件编写进行简单的入门,主要涉及了查找,删除和替换等基本操作。由于本人技术有限,如果有纰漏,请多多包涵和指出问题