-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Babel 是一个 JavaScript 编译器,可以将目前浏览器不支持的 JS 语法或者像 JSX,Flow 这种对 JS 的扩展编译成兼容性良好的代码。之前自己只是停留在学会配置上,这篇文章将试着通过一个简单的 babel 插件的实现理解一下 babel 的工作机制。
一、编译器
维基百科对编译器的解释是将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。从架构上来分通常可分为前端(front end)、中端(middle end)、后端(back end)。

- 前端:针对特定的源代码进行语法和语义检查,并最终生成一个源代码的中间层表示(IR,intermediate representation )。编译器前端通常又可以拆成三部分来看:词法分析(lexical analysis or lexing/tokenization), 语法分析(syntax analysis or scanning/parsing), 和语义分析( semantic analysis)。词法分析是将源程序转化为标记序列,包括扫描阶段和计算阶段;语法分析则解析转化好的标记序列并构建出解析树;语义分析则负责把语义信息加到构建好的解析树中,生成符号表,进行类型检查等
- 中端:分析(Analysis)和优化(Optimization)
- 后端:针对特定机器的优化(Machine dependent optimizations)和目标代码生成(Code generation)
二、babel 的工作过程
1. 总览
babel 对 JS 的转化也类似,它将源码转换 AST 之后(前端),通过遍历 AST树,对树做一些修改(中端),然后再将 AST 转成 目标代码(后端)。
2. Parse
babel 使用的解析器是 babylon,fork 自 acorn,babylon 现已经迁移到 babel 的 packages 中的 babel-parser 进行管理,可以将一段 JS code 解析为一个 AST(抽象语法树),是 babel-core 的核心依赖之一,使用方法如下:
// 将代码解析成 AST
const babylon = require('babylon')
const ast = babylon.parse('const a = 3; a + 5');
console.log(ast);结果如下(program 属性是生成的 AST):
所有的 AST 都会以 Pragram 作为根节点,包含程序中所有的顶级语句。上面的例子中只有两个:
- 一个
VariableDeclaration,由一个VariableDeclarator将NumericLiteral"3" 赋值给Identifier"a" - 一个
ExpressionStatement,由一个BinaryExpression组成,表示 了Identifier"a",operator"+" 和另一个NumericLiteral"5".
babel 关于 AST 的详细定义可见:babel-parser 。现实中的 AST 组成通常相当复杂,可以通过 https://astexplorer.net/ 这个 AST 直出工具来帮助自己理解一段程序解析出的 AST。
3. transform & generate
babylon 生成 AST 之后,便可以通过遍历 AST 做一些 transform,babel 遍历 AST 使用的是 babel-traverse ,使用方法如下:
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const code = `function square(n) {
return n * n
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x"
}
}
});
// 代码重新生成
const output = generate(ast)
console.log(output.code)通过节点的遍历可以操作生成的 AST,在遍历一个节点时,存在 enter 和 exit 两个时刻,enter 是在进入节点的回调函数,这时节点的子节点还没触达;exit 是在离开节点时,这时遍历子节点已完成。访问节点可以通过 path ,path 不等同于节点,其中重要的属性和方法如下:

上面的例子中使用 path 的 isIdentifier 判断节点性质和名称(这些构造、验证以及变换 AST 节点的方法其实来源于 babel-types ) ,并修改了节点的属性,通过 traverse 后的 AST 所有之前 name 为 "n" 的 Identifier 都会变成 "x",如上所示,babel 将 AST 生成目标代码的工具是 babel/generator ,重新生成 code 的结果如下:
- function square(n) {
- return n * n
- }
+ function square(x) {
+ return x * x
+ }三. 实现一个简易的 babel 插件
babel 的一个插件即是定义如何转换当前的一个节点,可以参考官方文档中提供的一个示例,这个插件的作用是将所有 Identifier 的名称逆转:
// my-plugin.js
module.exports = function () {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
// reverse the name: JavaScript -> tpircSavaJ
path.node.name = name.split("").reverse().join("");
}
}
};
}插件需要返回的是一个 vistor 对象定义访问遍历 AST 时的 node 节点的操作,之所以使用这个术语是遵循访问者模式 的概念。 vistor 中的每个函数接受两个参数,path 和 state 。编写代码验证插件的作用:
// index.js
const myPlugin = 'hello world';// .babelrc
{
"plugins": [
[
"./my-plugin"
]
]
}命令行执行:
babel index.js输出:
const nigulPym = 'hello world';
可以看到自定义的 my-plugin 将 Identifier 的名称逆转了。这是一个极简的示例,详细的如何对 AST 进行访问、转化操作可以参考 babel-handbook。



