概述
1. 起源
Babel >> 最初名为 6to5,顾名思义就是 es6 转 es5,但是后来随着 ECMAScript 标准的演进,有了 ES7、ES8、ESNext 等等, 6to5 的名字已经不合适了,因此团队再 2014 年决定将它重命名为 babel。
Babel 是 巴别塔 的意思,来自圣经中的典故:
人类曾试图建造通往天堂的高塔,上帝通过制造语言隔阂使协作失败,导致工程中止。这一隐喻巧妙呼应了 Babel 的使命——解决 JavaScript 不同版本间的"语言隔阂",让开发者能用最新语法编写跨环境兼容的代码。
2. 用途
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
// Babel 接收到的输入是: ES2015 箭头函数
[1, 2, 3].map(n => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
3. 转换工具
-
编译器(
Compiler):-
作用:将 高级语言(如 C、Java)转换为 低级语言(如汇编、机器码)
-
特点:
- 输出代码与硬件相关(如 CPU 指令、内存管理)。
- 需要开发者理解计算机底层原理(寄存器、内存操作)。
- 典型例子:
GCC(C/C++ 编译器)、LLVM(优化编译框架)。
-
-
转译器(
Transpiler):- 将高级语言(如 ES6+ JavaScript)转换为另一种高级语言(如 ES5 JavaScript)
- 输出代码仍然可读,适合开发者调试。
- 不涉及底层硬件,仅做语法转换。
- 典型例子:
Babel、TypeScript Compiler (tsc)、SWC(Rust 编写的 JS 转译器)。
- 将高级语言(如 ES6+ JavaScript)转换为另一种高级语言(如 ES5 JavaScript)
高级语言 vs. 低级语言 (了解即可)
| 特性 | 高级语言(如 JavaScript、Python) | 低级语言(如汇编、机器码) |
|---|---|---|
| 抽象程度 | 高(接近人类思维) | 低(接近机器指令) |
| 开发效率 | 高(易读、易维护) | 低(需关注底层细节) |
| 执行速度 | 较慢(需解释/编译) | 极快(直接运行) |
| 典型用途 | Web 开发、应用逻辑 | 嵌入式系统、驱动开发 |
结论:
Babel是一个 JavaScript 转译器(Transpiler),负责将新版 JavaScript(如 ES6+)转换为兼容性更好的旧版 JavaScript(如 ES5)。- 尽管 官网 >> 标题介绍 Babel 是一个 JavaScript 编译器,但个人认为,他是一个转译器!各位看官如何看呢 🤔
4. 编译流程
Babel 的编译流程主要包括以下几个步骤:
- 解析(Parsing):将源代码转换为抽象语法树(AST),构建代码的结构化表示。
- 转换(Transformation):通过插件系统遍历并修改 AST,实现语法降级或功能转换。
- 生成(Code Generation):将处理后的 AST 重新生成为目标版本的 JavaScript 代码。
简单理解,就是:Parse → Transform → Generate
AST - 抽象语法树
1. What's AST?
抽象语法树(Abstract Syntax Tree,AST)是对程序代码进行结构化表示的 树状数据结构,用于在编程语言处理和转换过程中进行代码分析和操作。
2. 用途
AST在前端无处不在,我们熟悉的开发工具几乎全依赖于AST进行开发:比如我们常用的babel插件将 es6 → es5 、ts → js 、代码压缩、CSS预处理器、eslint等等,他们在我们的实际开发中都是必不可少的,而他们的底层原理其实也都是AST。如果你想了解 js 编译执行(比如V8引擎的编译执行过程)的原理并掌握其精髓,那么你就必须得了解 AST。
3. AST 如何生成?
抽象语法树(AST)的生成过程主要包括以下步骤:
- 词法分析(Lexical Analysis):将源代码分解成词法单元(Tokens),如标识符、关键字、运算符等。
- 语法分析(Parsing):根据语法规则将词法单元组织成语法结构,构建初步的语法树。
- 语义分析(Semantic Analysis):对语法树进行静态语义检查,如变量声明检查、类型检查等,进一步完善语法树。
- 生成AST:根据完善的语法树规则,构建最终的抽象语法树,每个节点代表代码的不同部分(如表达式、语句、函数等),并建立它们之间的关系。
**注意:**不同的编程语言可能具有不同的语法规则和处理方式,因此 AST 的生成过程可能会有所差异。但总体上,AST 的生成是通过词法分析、语法分析和语义分析等步骤来构建具有结构化表示的代码树状结构。
3.1. 词法分析 — 分词
词法分析阶段主要负责将源代码拆分成词法单元,并为每个词法单元分配相应的标记,简单来说就是在源代码的基础上进行分词。
比如把 “我爱中国” 这段字符串根据某种规则拆解成 “我”、“爱”、“中国” 三部分,这个过程就叫做 分词,其中各个部分又被称为 词法单元 ,对于代码来说,需要将代码片段识别为关键字、标识符、操作符、字面量等部分,也就是分配标记。
我们可以这么理解,词法分析就是把你的代码从字符串类型转换成了数组,数组的元素就是代码里的单词(词法单元),并且标记了每个单词的类型。比如下面这段代码:
const a = 10;
词法分析器 从左往右逐个字符扫描分析 整个程序的字符串,当遇到不同的字符时,会驱使它迁移到不同的状态。在扫描字符的时候,遇到 c 字母,如果后面还有字符,将继续扫描,直到遇到空格,识别出 const,发现是一个关键字,将其生成词法单元:
{ type: 'Keyword', value: 'const' }
然后接着扫描,以此类推,生成 Token List。
所以,这段程序会被分解成为下面这些词法单元:let 、a、=、10、 ;,你可以在 这里 >> 查看词法分析结果:
[
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '10' },
{ type: 'Punctuator', value: ';' },
];
通过上面分解出来的词法分析结果,我们可以观察到,缺少一些比较关键的信息:
-
没有任何语法信息;
-
体现不了代码的执行顺序;
所以,需要进一步进行 语法分析。
3.2. 语法分析 — 解析
语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST)。同时,验证语法,语法如果有错的话,将抛出语法错误。
词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会在读取所有的词法记号后再使用语法分析器来处理。通常情况下,每取得一个词法记号,就将其送入语法分析器进行分析。
我们可以使用在线工具生成 AST:AST explorer >> 或者 Esprima.org >>,这里主要简单介绍下 AST explorer 的使用:
比如这一段代码:
function square(n) {
return n * n;
}
经过转化,输出 AST 结构如下:
你会留意到 AST 的每一层都拥有相同的结构:
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
提示:出于简化的目的移除了某些属性。
这样的每一层结构,被称为 节点,每一个节点都包含 type 属性,用于表示节点类型,比如:FunctionDeclaration、Identifier、BinaryExpression 等等。除此之外,Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置,比如: start、end、loc。
4. 节点类型
了解了AST结构的基本形状之后,接下来我们介绍AST节点类型,主要分为以下几个大类:字面量、标志符、语句、声明、表达式、注释 等等。
这里主要列举一些常用的类型,更多节点类型请参考 @babel/types >>
4.1. 字面量「Literal >>」
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
StringLiteral | 字符型字面量 | 通常指字符串类型的字面量:'Hello, AST!' |
NumericLiteral | 数值型字面量 | 通常指数字类型的字面量:123 |
BooleanLiteral | 布尔型字面量 | 通常指布尔类型值:true / false |
RegExpLiteral | 正则型字面量 | 通常指正则表达式:/[0-9]/ |
TemplatLiteral | 模板型字面量 | 通常指模板字符串(`) |
提示:这里主要展示常用的字面量类型,更多详情请参考这里 >>
4.2. 标志符「Identifier」
程序中所有的 变量名、函数名、对象键(key) 以及函数中的参数名,都属于标志符。
4.3. 语句「Statement >>」
语句是能够独立执行的基本单位,常见的语句类型有:
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
IfStatement | If 控制流语句 | 通常指 if (true) {} else {} |
ForInStatement | For-in 循环语句 | 通常指 for(let key in obj) {} |
SwitchStatement | Switch 语句 | 通常指 switch |
WhileStatement | While 循环语句 | 通常指 while(true) {} |
ForStatement | For 循环语句 | 通常指 for(let i = 0; i < 10; i++) {} |
BreakStatement | 中断语句 | 通常指 break |
ContinueStatement | 持续语句 | 通常指 continue |
ReturnStatement | 返回语句 | 通常指 return |
BlockStatement | 块语句 | 包裹在 {} 内的语句 |
ExpressionStatement | 表达式语句 | 通常为调用一个函数,比如 console.log(1) |
4.4. 声明「Declaration >>」
声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个 变量、函数、class、import、export 等。
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
VariableDeclaration | 变量声明 | const a = 10; |
FunctionDeclaration | 函数声明 | function sum() {} |
ImportDeclaration | 模块引入声明 | import { reactive } from 'vue;' |
ExportDefaultDeclaration | 模块默认导出声明 | export default a = 10; |
4.5. 表达式「Expression >>」
表达式的特点是执行完以后有返回值,这是和语句 (statement) 的区别
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
ArrayExpression | 数组表达式 | 通常指一个数组:[1, 2, 3] |
ArrowFunctionExpression | 箭头函数表达式 | () => {} |
AssignmentExpression | 赋值表达式 | 通常指为一个变量赋值,比如 a = 1 |
BinaryExpression | 二元表达式 | 1 + 2 |
UnaryExpression | 一元表达式 | -1 |
FunctionExpression | 函数表达式 | function(){} |
4.6. Comment & Program
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
Program | 程序主体 | 整段代码的主体 |
CommentBlock | 块级注释 | /* 块级注释 */ |
CommentLine | 单行注释 | // 单行注释 |
工作原理
Babel 的编译过程和大多数其他语言的编译器大致相同,可以分为 三个阶段:
📌:解析(Parse) → 转换(Transform) → 生成(Generate)。
1. 解析 — Parser
Babel 使用 @babel/parser 将源代码转换为抽象语法树(AST),这个过程分为两个关键阶段:
- 词法分析(Lexical Analysis)
- 将源代码字符串分解为有意义的语法单元(Tokens)
- 每个 Token 包含类型、值等元信息
- 类似将句子分解为单词的过程
- 语法分析(Syntax Analysis)
- 根据语法规则将 Tokens 流转换为树状结构
- 构建完整的 AST 表示
- 类似将单词组合成语法树的过程
2. 转换 — Transform
通过 @babel/traverse 对 AST 进行深度优先遍历和修改:
遍历机制
- 采用深度优先搜索(DFS)算法
- 访问每个 AST 节点并执行相应操作
转换操作
- 节点修改:更新节点属性
- 节点替换:用新节点替换现有节点
- 节点增删:插入或移除节点
插件系统
- 通过预设(Presets)和插件(Plugins)定义转换规则
- 支持自定义转换逻辑
3. 生成 — Generator
使用 @babel/generator 将转换后的 AST 输出为目标代码:
代码生成过程
- 深度优先遍历最终 AST
- 根据节点类型生成对应代码字符串
- 处理代码格式(缩进、分号等)
辅助功能
- 生成 Source Map 映射
- 支持代码压缩选项
- 保持代码可读性
输出控制
- 可配置生成选项
- 支持多种输出格式
- 保留注释等辅助信息
Babel APIs
我们知道 Babel 的编译流程分为三步:Parse → Transform → Generate,每一步都暴露了一些 Api 出来:
- 解析阶段:通过
@babel/parser将源码转成 AST; - 转换阶段:通过
@babel/traverse遍历AST,并调用visitor函数修改 AST,期间涉及到 AST 的判断、创建、修改等,这时候就需要@babel/types了,当需要批量创建 AST 的时候可以使用@babel/template来简化 AST 创建逻辑; - 生成阶段:通过
@babel/generate将 AST 输出为目标代码字符串,同时生成sourcemap; - 中途遇到错误想打印代码位置的时候,使用
@babel/code-frame包 - Babel 的整体功能通过
@babel/core提供,基于上面的包完成 Babel 整体的编译流程,并实现插件功能。
1. @babel/parser
@babel/parser(原Babylon)基于acorn实现,通过插件系统支持解析ESNext、JSX、Flow、TypeScript等语法,需显式启用非标准语法插件才能正确解析这些特性。示例如下:
// → 导入模块
const parser = require("@babel/parser");
// → 定义一段代码字符串
const codeString = `function square(n) {
return n * n;
}`;
// → 解析代码
const ast = parser.parse(codeString, {
sourceType: "module",
plugins: ["jsx", "flow"],
});
// → 输出解析结果
console.log(JSON.stringify(ast, null, 4));
2. @babel/traverse
@babel/traverse 是 Babel 提供的 AST 遍历工具,采用访问者模式(Visitor Pattern)实现对 AST 节点的精确操作
在遍历过程中对 AST 节点进行增删改,遍历一个节点的过程是:Enter → Traverse child node → Exit
语法形式如下:
require("@babel/traverse").default(ast, visitors)
ast:抽象语法树ASTvisitors:访问者
示例代码:
require("@babel/traverse").default(ast, {
/** - 1.局进入节点时触发(慎用) */
enter(path) {
console.log('__enter__');
},
/** - 2.全局离开节点时触发(慎用) */
exit(path) {
console.log('__exit__');
},
/** - 3.特定节点类型访问:当遍历到指定节点类型时调用,比如这里是:FunctionDeclaration(函数声明)(建议方案) */
FunctionDeclaration(path) {
console.log('__FunctionDeclaration__');
},
/** - 4.精确控制:特定节点类型的进入/离开 */
FunctionDeclaration: {
enter(path) {
console.log('__FunctionDeclaration_enter_');
},
exit(path) {
console.log('__FunctionDeclaration_exit_');
},
},
/** - 5.多类型匹配:使用|符号组合:当遍历到 FunctionDeclaration|ReturnStatement 节点时调用 */
/** - 这种方式会覆盖前面几种方式 */
['FunctionDeclaration|ReturnStatement'](path) {
console.log('__FunctionDeclaration|ReturnStatement');
},
});
最佳实践建议:
- 优先使用类型级访问,避免全局访问带来的性能损耗。
- 复杂场景可使用
|语法组合多个节点类型
参数 path:表示两个节点之间 连接 的 对象,包含关于路径操作和路径相关的信息( 后续在插件开发小节细说 )
3. @babel/types
@babel/types 是 Babel 生态中用于 AST 节点操作的核心工具库,提供节点创建、验证和转换等能力。
核心功能:
- 创建 AST 节点
- 验证节点类型
- 转换节点属性
- 构建复杂表达式
比如这里将刚刚示例函数里面的标志符 n 变成 x,示例如下:
// → 导入模块
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const t = require('@babel/types');
// → 定义一段代码字符串
const codeString = `function square(n) {
return n * n;
}`;
// → 解析代码
const ast = parser.parse(codeString, {
sourceType: "module",
plugins: ["jsx", "flow"],
});
// → 遍历节点
traverse.default(ast, {
Identifier(path) {
// 判断是否是 name 为 n 的标志符
if (t.isIdentifier(path.node, { name: 'n' })) {
path.node.name = 'x';
}
},
});
// → 输出ast
console.log(JSON.stringify(ast, null, 4));
提示:关于
@babel/types的更多用法请参考 API.
4. @babel/generator
@babel/generator 是 Babel 工具链中的代码生成器,负责将转换后的 AST 重新生成为可执行的 JavaScript 代码。
核心功能:
- 将 AST 转换为标准 JavaScript 代码
- 支持生成 Source Map
- 提供代码格式化选项
代码示例:
const generator = require('@babel/generator');
// ...
// → 将AST输出为目标代码
const { code, map } = generator(
ast,
{
// 生成选项(可选)
retainLines: false, // 是否保留原始行号
compact: false, // 是否压缩代码
comments: true, // 是否保留注释
sourceMaps: false, // 是否生成 source map
// 更多选项...
},
codeString // 原始代码(用于 source map)
);
console.log(code);
/**
* 输出结果示例:
* function square(x) {
* return x * x;
* }
*/
5. @babel/core
@babel/core 是 Babel 的核心编译引擎,整合了完整的编译流程(解析 → 转换 → 生成),提供多种编译方式。
核心功能:
- 完整的代码编译流水线
- 同步/异步编译支持
- 文件/代码/AST 多种输入方式
- 自动生成 sourcemap
-
同步编译
const { transformSync, transformFileSync, transformFromAstSync } = require('@babel/core'); // 从源代码编译 const result1 = transformSync('const x = 1;', { presets: ['@babel/preset-env'], sourceMaps: true }); // 从文件编译 const result2 = transformFileSync('input.js', { presets: ['@babel/preset-react'] }); // 从AST编译 const result3 = transformFromAstSync(parsedAst, sourceCode, { plugins: ['@babel/plugin-transform-arrow-functions'] }); -
异步编译
const { transformAsync, transformFileAsync, transformFromAstAsync } = require('@babel/core'); // 异步编译示例 async function compile() { const result = await transformAsync('class A {}', { presets: ['@babel/preset-env'] }); console.log(result.code); } -
输出结果结构:
所有编译方法都返回包含以下属性的对象:
{ code: string, // 生成的代码 map: object, // sourcemap ast: object, // 抽象语法树 metadata: object, // 元数据 sourceType: string // 源码类型(script/module) }
初探
1. 创建目录
创建一个基本的项目文件结构,并新建必要文件,如下所示:
$ mkdir -p babel && cd babel && touch app.js
注意:
package.json文件通过npm init -y指令自动生成。
2. 安装依赖
$ npm init -y
$ pnpm add --save-dev @babel/core @babel/cli @babel/preset-env
3. 配置文件
可以通过以下几种方式来使用配置文件:
babel.config.json:v7.8.0 以后(建议使用)babel.config.js:v7.8.0 以前(旧版本).babelrcpackage.json['babel']
核心配置参数
env:环境特定配置。plugins:插件集合(顺序执行)。presets:预设集合(逆序执行)。
配置文件解析规则
Babel 会从当前转译的文件所在目录下查找配置文件,如果没有找到,就顺着文档目录树一层层往上查找,一直到 .babelrc 文件存在或者带 babel 字段的 package.json 文件存在为止。
配置示例
接下来,在根目录中创建 babel.config.json 配置文件,并将以下内容复制到此文件中:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["> 0.5%", "last 2 versions", "not dead"]
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}
注意:如果你使用的是 Babel 的旧版本,则文件名为
babel.config.js。
const presets = [
[
"@babel/preset-env",
{
targets: {
browsers: ["> 0.5%", "last 2 versions", "not dead"],
},
useBuiltIns: "usage",
corejs: "3.6.5",
},
],
];
module.exports = { presets };
提示:上述浏览器列表(
browsers配置项)仅用于示例。请根据你所需要支持的浏览器进行调整。参见 此处 以了解@babel/preset-env可接受哪些参数。
4. 基本使用
通过上面的准备工作,我们现在就可以使用 Babel 进行编译转换了。在 /src/app.js 文件中写一个es6的箭头函数
(function () {
const hello = (name) => {
console.log(`Hello, ${name}!`);
};
hello('Babel');
})();
然后使用Babel命令行工具进行编译
# -- 编译文件
$ ./node_modules/.bin/babel app.js --out-file lib/app.js -w -s
# -- 编译目录
$ ./node_modules/.bin/babel src --out-dir lib -w -s
解读:
-
-o:将某个js文件编译成指定js文件 -
-d:将某个目录下的js文件编译至指定目录 -
-w:实时监听文件/自动编译 -
-s:生成资源映射文件便于调试,它可以帮助你在浏览器开发者工具(目前只有google chrome浏览器支持该功能)的“Source”选项卡中找到编译前的源文件,方便开发者进行调试。但首先得确保你开发者工具的设置里的这一项是处于勾选状态:
右键检查→工具栏中选择更多(右上角三个竖着的小圆点)→Setting→Sources→Enable JavaScript source maps.
经过编译后生成的 lib/app.js 是这样的:
(function () {
var hello = function hello(name) {
console.log("Hello, ".concat(name, "!"));
};
hello('Babel');
})();
5. 快捷指令
在 package.json 文件的 scripts 属性下,设置如下代码:
"scripts": {
"dev": "./node_modules/.bin/babel app.js --out-file lib/app.js -w -s"
},
提示:
dev这个属性名是自定义的,其属性值则是要执行的指令。
内容配置完成之后,切换到命令行窗口输入:
$ pnpm dev
这样即可执行指令进行编译。
插件开发基础
1. 语法结构
Babel 插件主要应用在转换(Transform)阶段,其本质就是一个函数,基本的语法结构如下:
module.exports = ({ types: t }) => ({
/** 插件名称 */
name: 'babel-plugin-name',
/** 访问者,插件核心代码主要在这里编辑 */
visitor: {
// → 访问指定节点类型时触发的钩子,这里是 Identifier 类型,当然也可以是其他任意节点类型
Identifier(path, state) {
// coding in here...
},
},
});
结构解读:
types:@babel/types >> 工具库,主要用来操作AST节点,比如创建、校验、转变等。name:插件名称。visitor:Babel 采取递归的方式访问AST的每个节点。Identifier:标识符,节点类型,当遍历到类型为Identifier时该函数会被触发。path:路径,节点之间连接对象,包含操作节点的一些列属性和方法。state:状态,你可以通过state访问插件的配置项。
2. Visitors(访问者)
当我们谈及 进入 一个节点,实际上是说我们在 访问 它们, 之所以使用这样的术语是因为有一个 访问者模式(Visitor)>> 的概念。
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个 对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。
const visitor = {
Identifier(path) {
console.log('Called!');
},
};
traverse.default(ast, visitor);
这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。比如下面这一段代码。
function square(n) {
return n * n;
}
观察其AST结构,Identifier 类型的节点有4个:
所以 Identifier() 方法会被调用四次:
Called!
Called!
Called!
Called!
3. Path(路径)
AST 通常会有许多节点,我们可以通过 path 对象表示节点之间的关联关系,所以,path 是表示两个节点之间连接的 对象。通过这个对象提供的属性方法,我们可以 操作 AST 语法树。
上图中红色方框处表示对应节点的 path 类型。
2.1. 属性
path.state:Babel 插件信息,可通过state.opts获取传入的 Options;path.node:当前遍历到的 node 节点,可通过它访问节点上的属性,对于 Ast 节点;path.parent:父级 node,无法进行替换;path.parentPath:父级 path,可进行替换;path.scope:作用域相关,可用于变量重命名,变量绑定关系检测等;path.key:获取路径所在容器的索引path.listKey:获取容器的keypath.container:获取路径的容器(包含所有同级节点的数组)path.inList:判断路径是否有同级节点
2.2. 方法
path.toString():当前路径所对应的源代码;path.isXXX:XXX为节点类型,可以判断是否符合节点类型。比如我们需要判断路径是否为StringLiteral类型 →path.isStringLiteral;path.get(key):获取子节点 path,例如:path.get('body.0')可以理解为path.node.body[0]这样的形式,让我们更加方便的拿到子路径,但是注意仅路径可以这样操作,访问属性是不允许的!path.set(key):设置子节点 path;path.remove():删除 path;path.replaceWith():用AST节点替换该节点,如:path.replaceWith({ type: 'NumericLiteral', value: 3 }),创建节点可以使用@babel/types,如果是多路径则使用relaceWithMultiple([AST...]);path.replaceWidthSourceString(): 用字符串替换源码path.find((path) => path.isObjectExpression()):向下搜寻节点path.findParent():向父节点搜寻节点path.getSibling()、path.getNextSibling()、path.getPrevSibling(): 获取兄弟路径;path.getFunctionParent(): 向上获取最近的 Function 类型节点;path.getStatementParent(): 向上获取最近的 Statement 类型节点;path.insertBefore():在之前插入兄弟节点path.insertAfter(): 在之后插入兄弟节点path.pushContainer(): 将AST push到节点属性里面path.traverse(): 递归的形式消除全局状态(官网 >> 例子已经不错了)path.stop(): 停止遍历path.skip(): 不往下遍历,跳过该节点
4. State(状态)
状态 >> 是抽象语法树AST转换的敌人,状态管理会不断牵扯你的精力,而且几乎所有你对状态的假设,总是会有一些未考虑到的语法最终证明你的假设是错误的。
考虑下列代码:
function square(n) {
return n * n;
}
让我们写一个把 n 重命名为 x 的访问者的快速实现.
let paramName;
let visitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
paramName = param.name;
param.name = 'x';
},
Identifier(path) {
if (path.node.name === paramName) {
path.node.name = 'x';
}
},
}
对于下面这段代码:
function square(n) {
return n * n;
}
n;
上面定义的访问者 visitor 也许能工作,但它很容易被打破,比如这里转换之后的结果是:
function square(n) {
return n * n;
}
n;
可以看到,和函数同级别的 n 也被转换成了 x,更好的处理方式是使用递归:
const updateParamNameVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) {
path.node.name = 'x';
}
},
};
const visitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
paramName = param.name;
param.name = 'x';
path.traverse(updateParamNameVisitor, { paramName });
},
};
traverse.default(ast, visitor);
输出结果:
function square(x) {
return x * x;
}
n;
达到预期,当然,这只是一个刻意编写的例子,不过它演示了如何从访问者中消除全局状态。
5. Scope(作用域)
JavaScript 支持 词法作用域 ,当创建引用时,不管是通过变量、函数、类型、参数、模块导入还是标签等,它都属于当前作用域。
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}
1)更深的内部作用域代码可以使用外层作用域中的引用,比如在 scope 2 中,可以访问 scope 1 和 global scope 里面的引用。
2)内层作用域也可以创建和外层作用域同名的引用。比如在 scope 1 中定义了一个变量 a,在 scope 2 也可以定义一个同名的变量 a。
function scopeOne() {
const a = 1;
const b = 2;
function scopeTwo() {
// → 访问外层作用域里面的变量
console.log(a);
// → 创建相同名称的变量
const b = 3;
}
}
当编写一个转换时,必须小心作用域。我们得确保在改变代码的各个部分时不会破坏已经存在的代码。
作用域可以被表示为如下形式:
{
path: NodePath,
block: Node,
parentBlock: Node,
parent: Scope,
bindings: { [name: string]: Binding }
}
@Bindings(绑定)
所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding),通过 path.scope.bindings 可以查看变量被绑定的情况。
相关API:
-
path.scope.hasBinding("n"):检查本地n变量是否被绑定(定义)。 -
path.scope.hasOwnBinding("n"): 检查自身作用域里面有没有绑定(定义)过n变量。 -
path.scope.generateUidIdentifier("uid"): 生成一个当前作用域下肯定不存在的变量名称。 -
path.scope.rename("n", "x"):重命名变量。 -
path.scope.parent.push:(节点) 在父作用域里面插入一个节点
7. 插件开发流程(环境)
通常,我们在开发一个插件的时候可以这样做:
1)创建必要的项目文件,比如:
$ mkdir babel-plugin-xxx && cd babel-plugin-xxx && touch index.js && touch __test__.js
index.js:插件代码__test__.js:测试代码
2)安装依赖
$ npm init -y && npm --save-dev install @babel/core @types/node
3)编写插件基础代码
module.exports = ({ types: t }) => ({
/** 插件名称 */
name: 'babel-plugin-xxx',
/** 访问者 */
visitor: {
// coding in here...
},
});
4)编写测试代码
// → 导入 transformSync
const { transformSync } = require('@babel/core');
// → 编写测试代码
const code = `var a = 10;`;
// → 调用插件处理code
const output = transformSync(code, {
plugins: ['./index.js'],
});
// → 输出目标代码
console.log(output.code);
4)通过node运行
$ node __test__.js
特别提示:在插件开发的过程中,你应该随时打开 AST explorer >> 查看你要处理的源代码的 AST 结构,这样便于你处理插件中的逻辑。
插件开发实战
babel-plugin-del-consolelogs
module.exports = ({ types: t }) => {
return {
/** 插件名称 */
name: 'babel-plugin-del-consolelogs',
/** 访问者 */
visitor: {
CallExpression(path) {
// → 生产环境下才处理注释清除
if (process.env.NODE_ENV === 'production') {
// → 获取父级AST节点
const parentNode = path.parent;
// → 筛选是否是打印语句
const isConsoleStatement = t.isIdentifier(path.node.callee.object, {
name: 'console',
});
// → 定义变量,记录是否执行清除动作,默认为true
let shouldRemove = true;
// → 判断是否是conosle语句
if (isConsoleStatement) {
// → 遍历前置注释
if (parentNode.leadingComments) {
parentNode.leadingComments.forEach((comment) => {
// 判断注释内容是否是保留注释:@lg-noremove
if (comment.value.trim() === '@lg-noremove') {
shouldRemove = false;
}
});
}
// → 遍历后置注释
if (parentNode.trailingComments) {
// 获取console所在行
const consoleLine = parentNode.expression.loc.start.line;
parentNode.trailingComments.forEach((comment) => {
// 读取注释所在行
const commentLine = comment.loc.start.line;
// 判断注释内容是否是保留注释:@lg-noremove 并且是否和console语句在同一行
if (
comment.value.trim() === '@lg-noremove' &&
consoleLine === commentLine
) {
shouldRemove = false;
}
});
}
// → 移除
if (shouldRemove) {
path.remove();
}
}
}
},
},
};
};