前言
本文重点讲述如何写一个babel插件,对于一些知识概念并没有过多的描述,废话少说啦,直接奔入主题。
写一个这样的插件
比如我们有以下代码:
const a = 1;
const b = 10;
const c = (d) => {
var e = 20;
console.log(d);
}
我们要写一个插件,主要是把上面代码 const 转换成 var , 把箭头函数转换成 function,最后再把箭头函数里的形参及函数体内的变量d改成变量m。即转换后代码如下:
var a = 1;
var b = 10;
var c = function (m) {
var e = 20;
console.log(m);
};
一些准备知识点
babel是什么
babel可以看作是一个转换器,可以把一些代码转成另外一些代码。其转换过程可以分为3个步骤:
- 源码解析生成AST
- AST转换成新的AST(babel插件在此过程对代码进行改造)
- 新的AST生成新的代码
AST(抽象语法树)
AST是一颗描述代码的树,其中每个节点代表代码中某一个位置的意思,比如通过 astexplorer 将代码const a = 1;
转换为AST,可得到如下一棵节点树:
从图中,可以看到每个节点都有一个type字段,type正是代表每个节点是那种类型的节点,AST的type远不止图中列举的三种,更多的type可以到这里 手册 查阅。
babel插件一些关键方法及工具
-
Visitor
Visitor对象就类似监听方法的大集合,当babel在处理AST每个节点时,如果在Visitor有声明某个节点类型的方法,当babel处理AST节点的类型和此节点类型方法一致就会执行方法,比如:
visitor: { Identifier(path) { // 当节点类型为identifier,此方法就会执行。 console.log('type is identifier') } }
-
Path
上面的visitor对象声明的方法中,第一个参数是一个名为path参数,这个path参数中包含了节点的信息以及对节点的添加、更新、移动和删除节等方法,更多关于path定义可以直接查看path源码进行查阅。
-
babel-types
babel-types用于处理AST节点的工具库,包含了构造、验证AST节点等方法。更多信息可以到babel官网查阅。
开始写插件
上述描述的插件功能可以分为3点:
- 将const转换为var
- 将箭头函数转换为function函数
- 将箭头函数里的形参及函数体内的变量d改成变量m
接下来我们一一来实现,首先是将const转换为var:
-
添加visitor对象的节点监听方法
上文我们提到我们需要在visitor对象里添加我们对应的类型方法,但是有一个问题,要如何知道我们所需要的类型对应方法写法呢?其实这个可以将我们的源码放到astexplorer 转换以下成AST,然后找到对应的type字段,就可以知道我们要在visitor对象里写哪些方法。这里我们将上文提到的要转换源码生成AST:
var a = 1; var b = 10; var c = function (m) { var e = 20; console.log(m); };
从AST中可以看到body下面有三个类型为VariableDeclaration的数组元素,而VariableDeclaration类型正是对应变量声明的一种类型,查看下babel-types 手册,看下定义:
可以看到babel-types里定义了变量的构造方法
t.variableDeclaration(kind, declarations)
,其中参数kind可以取"var" | "let" | "const"
。所以这里我们要在visitor对象添加监听类型方法,便是
VariableDeclaration
:visitor: { VariableDeclaration(path) { console.log(path) } }
-
转换AST
上文我们已经在visitor添加了针对变量的监听方法,而且也知道节点为
VariableDeclaration
类型有一个变量为kind标识着声明变量的方式(从上文AST中也可以看出来,即kind: "const"
),所以我们这里是要将const
转换为var
,那可以直接在visitor里监听的VariableDeclaration
方法将该节点kind
赋值为var
即可:visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } } }
运行下代码即可看到我们所需要的效果:
上图中我们已经成功将
const
转换为var
,转换方式就是将节点的kind
字段直接赋值为var
,除了这种方式,我们也可以通过替换当前节点的方式,来达到同样的效果。那如何构造呢,上文说过 “babel-types用于处理AST节点的工具库,包含了构造、验证AST节点等方法” ,所以我们可以直接用babel-types定义的构造方法来构造一个variableDeclaration
节点,具体方法在上文已经截图展示了,这里直接上代码:visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var // types 为 babel-types工具库 let node = types.VariableDeclaration('var', path.node.declarations) // replaceWith为替换节点的方法 path.replaceWith(node); } } }
上述代码也可以达到同样的转换效果,其中
replaceWith
这个方法为替换节点的方法,可在源码中查看使用方法。
接下来我们要来实现箭头函数转换为function函数功能,同样按照上面两个步骤
-
添加visitor对象的节点监听方法
同样方式,我们还是从AST树上对应的箭头函数类型的监听方法的写法
从AST中可以看出箭头函数类型是
ArrowFunctionExpression
,所以visitor里的写法:visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } }, ArrowFunctionExpression(path) { console.log(path) } }
-
转换AST
要将箭头函数转换为function函数,显然是需要使用babel-types定义function函数构造方法来构造一个function函数节点,然后再将原来节点替换掉即可,所以我们得首先查一下function函数构造方法的api是啥样子的:
从图中我们知道了function函数构造方法api,那么我们就可以直接来进行转换了:
visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } }, ArrowFunctionExpression(path) { path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body)); } }
看下效果
上面两个功能都实现了,接下来我们来实现下如何将箭头函数里的形参及函数体内的变量d改成变量m。还是按照上面两个步骤来:
-
添加visitor对象的节点监听方法
还是从AST中找到对应的箭头函数类型的监听方法的写法
即:Identifier
所以监听方式如下:
visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } }, ArrowFunctionExpression(path) { path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body)); }, Identifier(path) { console.log(path) } }
-
转换AST
从AST中我们知道Identifier类型的节点有一个
name
属性来标识该节点的名称,所以我们要改变此节点名称,就是要对name
属性进行重新赋值,即:visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } }, ArrowFunctionExpression(path) { path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body)); }, Identifier(path) { if (path.node.name === 'd') { // 如果变量名是d,则调整为m path.node.name = 'm'; } } }
看下效果:
如图所示,变量d也被成功转换m,但是这里有个问题,假设我在源码中箭头函数外也声明了一个d变量,那也这个d变量也会改成m,例如源码为:
const a = 1; const b = 10; const c = (d) => { var e = 20; console.log(d); } const d = 30;
执行下上述代码:
可以看到我们的插件也把其他地方的d变量改成m了,所以这里需要调整下转换代码,因为我们只需要转换箭头函数内的d变量,因此我们应该在箭头函数的监听方式对当前作用域内进行转换即可:
visitor: { VariableDeclaration(path) { if(path.node.kind === 'const') { // 如果kind为const,则转换为var path.node.kind = 'var' } }, ArrowFunctionExpression(path) { // 对当前节点进行traverse path.traverse({ Identifier(path) { if (path.node.name === 'd') { path.node.name = 'm'; } } }); path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body)); } }
执行下代码
这下,箭头函数外的变量d就不会被转换了。
总结
到这里,我们就完成一个babel插件了,源码请点击这里获取:github.com/canfoo/babe…
可能有些读者会认为本文实现的插件功能比较简单,也确实比较简单,但是本文的目的主要是表述babel插件是开发方式,所以希望小小经验对你们有帮助的😊。