如何写一个Babel插件
前言
之前看到一位大佬的博客, 介绍了babel的原理, 以及如何写一个babel的插件, 抱着试试看的想法, 照葫芦画瓢的自己写了一个简单的babel插件, 该插件的作用就是将代码字符串中的表达式, 直接转换为对应的计算结果。例如: const code = const result = 1 + 1
转化为const code = const result = 2
。当然这一篇文章非常的浅显, 但是对了解Babel的原理以及AST的基本概念是足够的了。
相关链接
插件的源码
const t = require('babel-types') const visitor = { // 二元表达式类型节点的访问者 BinaryExpression(path) { // 子节点 // 访问者会一层层遍历AST抽象语法树, 会树形遍历AST的BinaryExpression类型的节点 const childNode = path.node let result = null if ( t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right) ) { const operator = childNode.operator switch (operator) { case '+': result = childNode.left.value + childNode.right.value break case '-': result = childNode.left.value - childNode.right.value break case '/': result = childNode.left.value / childNode.right.value break case '*': result = childNode.left.value * childNode.right.value break } } if (result !== null) { // 替换本节点为数字类型 path.replaceWith( t.numericLiteral(result) ) if (path.parentPath) { const parentType = path.parentPath.type if (visitor[parentType]) { visitor[parentType](path.parentPath) } } } }, // 属性表达式 MemberExpression(path) { const childNode = path.node let result = null if ( t.isIdentifier(childNode.object) && t.isIdentifier(childNode.property) && childNode.object.name === 'Math' ) { result = Math[childNode.property.name] } if (result !== null) { const parentType = path.parentPath.type if (parentType !== 'CallExpression') { // 替换本节点为数字类型 path.replaceWith( t.numericLiteral(result) ) if (visitor[parentType]) { visitor[parentType](path.parentPath) } } } }, // 一元表达式 UnaryExpression (path) { const childNode = path.node let result = null if ( t.isLiteral(childNode.argument) ) { const operator = childNode.operator switch (operator) { case '+': result = childNode.argument.value break case '-': result = -childNode.argument.value break } } if (result !== null) { // 替换本节点为数字类型 path.replaceWith( t.numericLiteral(result) ) if (path.parentPath) { const parentType = path.parentPath.type if (visitor[parentType]) { visitor[parentType](path.parentPath) } } } }, // 函数执行表达式 CallExpression(path) { const childNode = path.node // 结果 let result = null // 参数的集合 let args = [] // 获取函数的参数的集合 args = childNode.arguments.map(arg => { if (t.isUnaryExpression(arg)) { return arg.argument.value } }) if ( t.isMemberExpression(childNode.callee) ) { if ( t.isIdentifier(childNode.callee.object) && t.isIdentifier(childNode.callee.property) && childNode.callee.object.name === 'Math' ) { result = Math[childNode.callee.property.name].apply(null, args) } } if (result !== null) { // 替换本节点为数字类型 path.replaceWith( t.numericLiteral(result) ) if (path.parentPath) { const parentType = path.parentPath.type if (visitor[parentType]) { visitor[parentType](path.parentPath) } } } } } module.exports = function () { return { visitor } }
基本概念
建议先阅读一下这一篇文档
babel工作的原理
Babel对代码进行转换,会将JS代码转换为AST抽象语法树(解析),对树进行静态分析(转换),然后再将语法树转换为JS代码(生成)。每一层树被称为节点。每一层节点都会有type属性,用来描述节点的类型。其他属性用来进一步描述节点的类型。
// 将代码生成对应的抽象语法树 // 代码 const result = 1 + 1 // 代码生成的AST { "type": "Program", "start": 0, "end": 20, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 20, "declarations": [ { "type": "VariableDeclarator", "start": 6, "end": 20, "id": { "type": "Identifier", "start": 6, "end": 12, "name": "result" }, "init": { "type": "BinaryExpression", "start": 15, "end": 20, "left": { "type": "Literal", "start": 15, "end": 16, "value": 1, "raw": "1" }, "operator": "+", "right": { "type": "Literal", "start": 19, "end": 20, "value": 1, "raw": "1" } } } ], "kind": "const" } ], "sourceType": "module" }
解析
解析分为词法解析和语法分析, 词法解析将代码字符串生成令牌流, 而语法分析则会将令牌流转换成AST抽象语法树
转换
节点的路径(path)对象上, 会暴露很多添加, 删除, 修改AST的API, 通过操作这些API实现对AST的修改
生成
生成则是通过对修改后的AST的遍历, 生成新的源码
遍历
AST是树形的结构, AST的转换的步骤就是通过访问者对AST的遍历实现的。访问者会定义处理不同的节点类型的方法。遍历树形结构的同时,, 遇到对应的节点类型会执行相对应的方法。
访问者
Visitors访问者本身就是一个对象,对象上不同的属性, 对应着不同的AST节点类型。例如,AST拥有BinaryExpression(二元表达式)类型的节点, 如果在访问者上定义BinaryExpression属性名的方法, 则这个方法在遇到BinaryExpression类型的节点, 就会执行, BinaryExpression方法的参数则是该节点的路径。注意对每一个节点的遍历会执行两次, 进入节点一次, 退出节点一次
const visitors = { enter (path) { // 进入该节点 }, exit (path) { // 退出该节点 } }
路径
每一个节点都拥有自身的路径对象(访问者的参数, 就是该节点的路径对象), 路径对象上定义了不同的属性和方法。例如: path.node代表了该节点的子节点, path.parent则代表了该节点的父节点。path.replaceWithMultiple方法则定义的是替换该节点的方法。
访问者中的路径
节点的路径信息, 存在于访问者的参数中, 访问者的默认的参数就是节点的路径对象
第一个插件
我们来写一个将const result = 1 + 1
字符串解析为const result = 2
的简单插件。我们首先观察这段代码的AST, 如下。
我们可以看到BinaryExpression类型(二元表达式类型)的节点, 中定义了这段表达式的主体(1 + 1), 1 分别是BinaryExpression节点的子节点left,BinaryExpression节点的子节点right,而加号则是BinaryExpression节点的operator的子节点
// 经过简化之后 { "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "result" }, "init": { "type": "BinaryExpression", "left": { "type": "Literal", "value": 1 }, "operator": "+", "right": { "type": "Literal", "value": 1 } } } ] } ] }
接下来我们来处理这个类型的节点,代码如下
const t = require('babel-types') const visitor = { BinaryExpression(path) { // BinaryExpression节点的子节点 const childNode = path.node let result = null if ( // isNumericLiteral是babel-types上定义的方法, 用来判断节点的类型 t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right) ) { const operator = childNode.operator // 根据不同的操作符, 将left.value, right.value处理为不同的结果 switch (operator) { case '+': result = childNode.left.value + childNode.right.value break case '-': result = childNode.left.value - childNode.right.value break case '/': result = childNode.left.value / childNode.right.value break case '*': result = childNode.left.value * childNode.right.value break } } if (result !== null) { // 计算出结果后 // 将本身的节点,替换为数字类型的节点 path.replaceWith( t.numericLiteral(result) ) } } }
我们定义一个访问者, 在上面定义BinaryExpression的属性的方法。运行结果如我们预期, const result = 1 + 1被处理为了const result = 2。但是我们将代码修改为const result = 1 + 2 + 3发现结果变为了 const result = 3 + 3, 这是为什么呢?
我们来看一下1 + 2 + 3的AST抽象语法树.
// 经过简化的AST type: 'BinaryExpression' - left - left - left type: 'Literal' value: 1 - opeartor: '+' - right type: 'Literal' value: 2 - opeartor: '+' - right type: 'Literal' value: 3
我们上面的代码的判断条件是。t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right), 在这里只有最里层的AST是满足条件的。因为整个AST结构类似于, (1 + 2) + 3 => (left + rigth) + right。
解决办法是,将内部的 1 + 2的节点替换成数字节点3之后,将数字节点3的父路径(parentPath)重新执行BinaryExpression的方法(数字类型的3节点和right节点), 通过递归的方式,替换所有的节点。修改后的代码如下。
BinaryExpression(path) { const childNode = path.node let result = null if ( t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right) ) { const operator = childNode.operator switch (operator) { case '+': result = childNode.left.value + childNode.right.value break case '-': result = childNode.left.value - childNode.right.value break case '/': result = childNode.left.value / childNode.right.value break case '*': result = childNode.left.value * childNode.right.value break } } if (result !== null) { // 替换本节点为数字类型 path.replaceWith( t.numericLiteral(result) ) BinaryExpression(path.parentPath) } }
结果如我们预期, const result = 1 + 2 + 3 可以被正常的解析。但是这个插件还不具备对Math.abs(), Math.PI, 有符号的数字的处理,我们还需要在访问者上定义更多的属性。最后, 对于Math.abs函数的处理可以参考上面的源码.