Babel 代码转换原理

675 阅读5分钟

日常开发我们几乎都要用到 Babel,将 JavaScript 新的语法编译成老的浏览器能够识别的语法。了解它的原理有助于我们对它的使用。其中最核心的就是 AST,有了 AST 我们就能将 let 转变成 var,将箭头函数转换成普通函数,还可以去除开发调试时添加的 console.log() debugger,基本涉及到修改源码的都需要用到它。

AST

AST 可以简单理解成是用来表示源码的树形结构,树的每一层都对应了源码的一部分。下图来源于 astexplorer

图片.png 图中左侧是源码,右侧是 AST,可以看到这里的 AST 是一个树形结构。树的每一层是一个节点,而每一个节点都对应了源码的一部分。黄色背景部分对应的是所有源码,红框对应了源码第一行的变量声明,蓝框对应了源代码第二行的变量声明。
每个节点都有自己的类型。类型名称就存储在 type 属性中,类型名称和这个节点对应的代码的行为有关,比如红色框对应的代码是变量声明,所以这个节点的名称就叫 VariableDeclaration。下图中节点对应的是一个函数声明语句,所以它的类型就叫 FunctionDeclaration。节点的类型非常多,完整的类型列表参考 babel ast spec

图片.png

节点之间是存在嵌套关系的。还是以变量声明为例,这一行包含了声明变量的关键字,变量名称标识符和变量值。下图中可以看到变量名称和变量值对应的节点是变量声明的子节点。 图片.png

源码如何变成 AST

  1. 既然 AST 的每一层表示了源码的一部分,所以我们首先要对源码进行拆分,拆分得到的每一部分成为 token。并且将 这些 token 存放在一个数组里。
  2. 有了 token 数组后,就可以遍历数组生成 AST 对象了。 详细的过程参考这个就好了 the-super-tiny-compiler

代码转换

有了 AST 之后我们看一下如何去修改源代码。这里我们不会自己去写AST生成器,而是用下面三个包

  1. @babel/parser 用来生成 AST 对象
  2. @babel/traverse 用来遍历 AST 对象,对 AST 对象进行修改
  3. @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的属性。

图片.png

我想你应该知道如何将 let 修改为 var 了。没错就是修改这个属性值为 var

// 遍历修改 AST
traverse(ast, {
    VariableDeclaration: function(path) {
        // 修改 AST 节点的 kind 属性为 var
        let node = path.node;
        node.kind = "var"
    }
})

自动去除开发时添加的 debugger

首先要确定 debugger 对应的 AST 节点类型。

图片.png 上图可以看到 debugger 对应的节点类型叫 DebuggerStatement,所以我们在 traverse 方法的第二个参数中加上名为 DebuggerStatement 的函数。

// 遍历修改 AST
traverse(ast, {
    // 这个对象的key 值对应的是 AST 节点类型
    DebuggerStatement: function(path) {
        // 调用 path 上 remove 方法,移除当前节点
        path.remove()
    }
})

这里我们使用了 path 对象上一个 remove 方法,该方法会从 AST 中移除当前的节点。当然 path 上还包含了很多其他的操作节点的方法,但是我没有在官方文档上找他其他的方法,不过你可以通过源码来看下这个对象上还包含了哪些其他的方法。

图片.png

下面这三个是我查看源码找到的

  1. path.replaceWith() 替换为新的节点
  2. path.remove() 删除当前节点
  3. path.skip() 跳过子节点

总结

了解了 AST,@babel/parser,@babel/traverse,@babel/generator 后我想你现在已经对 babel 转换代码的原理有了大体的了解。现在你再去配置 babel 或者装备写一个自己的 babel 插件(比如去除代码中的console)应该很容易就上手了。赶紧尝试一下吧,动手操作一下是最有效的学习方法。