乐趣区

如何写一个Babel插件

前言
之前看到一位大佬的博客, 介绍了 babel 的原理, 以及如何写一个 babel 的插件, 抱着试试看的想法, 照葫芦画瓢的自己写了一个简单的 babel 插件, 该插件的作用就是将代码字符串中的表达式, 直接转换为对应的计算结果。例如: const code = const result = 1 + 1 转化为 const code = const result = 2。当然这一篇文章非常的浅显, 但是对了解 Babel 的原理以及 AST 的基本概念是足够的了。
相关链接

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 函数的处理可以参考上面的源码.

退出移动版