日常开发我们几乎都要用到 Babel,将 JavaScript 新的语法编译成老的浏览器能够识别的语法。了解它的原理有助于我们对它的使用。其中最核心的就是 AST,有了 AST 我们就能将 let 转变成 var,将箭头函数转换成普通函数,还可以去除开发调试时添加的 console.log() debugger,基本涉及到修改源码的都需要用到它。
AST
AST 可以简单理解成是用来表示源码的树形结构,树的每一层都对应了源码的一部分。下图来源于 astexplorer
图中左侧是源码,右侧是 AST,可以看到这里的 AST 是一个树形结构。树的每一层是一个节点,而每一个节点都对应了源码的一部分。黄色背景部分对应的是所有源码,红框对应了源码第一行的变量声明,蓝框对应了源代码第二行的变量声明。
每个节点都有自己的类型。类型名称就存储在 type 属性中,类型名称和这个节点对应的代码的行为有关,比如红色框对应的代码是变量声明,所以这个节点的名称就叫 VariableDeclaration。下图中节点对应的是一个函数声明语句,所以它的类型就叫 FunctionDeclaration。节点的类型非常多,完整的类型列表参考 babel ast spec
节点之间是存在嵌套关系的。还是以变量声明为例,这一行包含了声明变量的关键字,变量名称标识符和变量值。下图中可以看到变量名称和变量值对应的节点是变量声明的子节点。
源码如何变成 AST
- 既然 AST 的每一层表示了源码的一部分,所以我们首先要对源码进行拆分,拆分得到的每一部分成为 token。并且将 这些 token 存放在一个数组里。
- 有了 token 数组后,就可以遍历数组生成 AST 对象了。 详细的过程参考这个就好了 the-super-tiny-compiler
代码转换
有了 AST 之后我们看一下如何去修改源代码。这里我们不会自己去写AST生成器,而是用下面三个包
@babel/parser用来生成 AST 对象@babel/traverse用来遍历 AST 对象,对 AST 对象进行修改@babel/generator根据修改后的 AST 对象生成新的代码 这三个包的作用其实已经说明了 babel 转换代码的过程。他们是 babel 内部使用的,我们也可以直接安装自己玩。
先安装上,然后在代码里引用,这里我是用 nodejs 来跑的,如果你用的是 esModule 请参考官网的引用方法。 @babel/traverse,@babel/generator
// commonjs
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const generate = require("@babel/generator").default
// es
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
使用三个包提供的方法
// 需要转换的代码,这里直接写死,当然你可以用 nodejs 的 file API来获取某个文件的内容
let code = "let a = 1;"
// 生成 AST
let ast = parser.parse( code );
// 遍历修改 AST
traverse(ast, {
// 这个对象的key 值对应的是 AST 节点类型
VariableDeclaration: function(path) {
// 修改 AST 节点的代码
}
})
// 生成新的代码
let output = generate( ast )
console.log( output.code )
traverse 方法的第二个参数是一个对象,对象的属性名对应一种节点类型,属性值是一个函数,接受一个 path 参数,这个 path 其实是 babel 对 AST 节点的包装,可以通过访问 path.node 访问节点。现在我们希望将源代码中的 let 关键字替换成 var 关键字,那我们的属性名就应该叫 VariableDeclaration,接着看该类型节点有一个叫 kind的属性。
我想你应该知道如何将 let 修改为 var 了。没错就是修改这个属性值为 var。
// 遍历修改 AST
traverse(ast, {
VariableDeclaration: function(path) {
// 修改 AST 节点的 kind 属性为 var
let node = path.node;
node.kind = "var"
}
})
自动去除开发时添加的 debugger
首先要确定 debugger 对应的 AST 节点类型。
上图可以看到 debugger 对应的节点类型叫 DebuggerStatement,所以我们在 traverse 方法的第二个参数中加上名为 DebuggerStatement 的函数。
// 遍历修改 AST
traverse(ast, {
// 这个对象的key 值对应的是 AST 节点类型
DebuggerStatement: function(path) {
// 调用 path 上 remove 方法,移除当前节点
path.remove()
}
})
这里我们使用了 path 对象上一个 remove 方法,该方法会从 AST 中移除当前的节点。当然 path 上还包含了很多其他的操作节点的方法,但是我没有在官方文档上找他其他的方法,不过你可以通过源码来看下这个对象上还包含了哪些其他的方法。
下面这三个是我查看源码找到的
- path.replaceWith() 替换为新的节点
- path.remove() 删除当前节点
- path.skip() 跳过子节点
总结
了解了 AST,@babel/parser,@babel/traverse,@babel/generator 后我想你现在已经对 babel 转换代码的原理有了大体的了解。现在你再去配置 babel 或者装备写一个自己的 babel 插件(比如去除代码中的console)应该很容易就上手了。赶紧尝试一下吧,动手操作一下是最有效的学习方法。