Babel & AST(抽象语法树)

6,206 阅读16分钟

概述

1. 起源

Babel >> 最初名为 6to5,顾名思义就是 es6es5,但是后来随着 ECMAScript 标准的演进,有了 ES7、ES8、ESNext 等等, 6to5 的名字已经不合适了,因此团队再 2014 年决定将它重命名为 babel

Babel 是 巴别塔 的意思,来自圣经中的典故:

人类曾试图建造通往天堂的高塔,上帝通过制造语言隔阂使协作失败,导致工程中止。这一隐喻巧妙呼应了 Babel 的使命——解决 JavaScript 不同版本间的"语言隔阂",让开发者能用最新语法编写跨环境兼容的代码。

2. 用途

  1. 语法转换
  2. 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js
  3. 源码转换(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)
      • 输出代码仍然可读,适合开发者调试。
      • 不涉及底层硬件,仅做语法转换。
      • 典型例子:BabelTypeScript Compiler (tsc)SWC(Rust 编写的 JS 转译器)。

高级语言 vs. 低级语言 (了解即可)

特性高级语言(如 JavaScript、Python)低级语言(如汇编、机器码)
抽象程度高(接近人类思维)低(接近机器指令)
开发效率高(易读、易维护)低(需关注底层细节)
执行速度较慢(需解释/编译)极快(直接运行)
典型用途Web 开发、应用逻辑嵌入式系统、驱动开发

结论:

  1. Babel 是一个 JavaScript 转译器(Transpiler,负责将新版 JavaScript(如 ES6+)转换为兼容性更好的旧版 JavaScript(如 ES5)。
  2. 尽管 官网 >> 标题介绍 Babel 是一个 JavaScript 编译器,但个人认为,他是一个转译器!各位看官如何看呢 🤔

4. 编译流程

Babel 的编译流程主要包括以下几个步骤:

  1. 解析(Parsing):将源代码转换为抽象语法树(AST),构建代码的结构化表示。
  2. 转换(Transformation):通过插件系统遍历并修改 AST,实现语法降级或功能转换。
  3. 生成(Code Generation):将处理后的 AST 重新生成为目标版本的 JavaScript 代码。

简单理解,就是:ParseTransformGenerate

AST - 抽象语法树

1. What's AST?

javascript_ast.png

抽象语法树Abstract Syntax Tree,AST)是对程序代码进行结构化表示的 树状数据结构,用于在编程语言处理和转换过程中进行代码分析和操作。

2. 用途

AST在前端无处不在,我们熟悉的开发工具几乎全依赖于AST进行开发:比如我们常用的babel插件将 es6 → es5 、ts → js 、代码压缩、CSS预处理器、eslint等等,他们在我们的实际开发中都是必不可少的,而他们的底层原理其实也都是AST。如果你想了解 js 编译执行(比如V8引擎的编译执行过程)的原理并掌握其精髓,那么你就必须得了解 AST。

3. AST 如何生成?

抽象语法树(AST)的生成过程主要包括以下步骤:

  1. 词法分析(Lexical Analysis):将源代码分解成词法单元(Tokens),如标识符、关键字、运算符等。
  2. 语法分析(Parsing):根据语法规则将词法单元组织成语法结构,构建初步的语法树。
  3. 语义分析(Semantic Analysis):对语法树进行静态语义检查,如变量声明检查、类型检查等,进一步完善语法树。
  4. 生成AST:根据完善的语法树规则,构建最终的抽象语法树,每个节点代表代码的不同部分(如表达式、语句、函数等),并建立它们之间的关系。

**注意:**不同的编程语言可能具有不同的语法规则和处理方式,因此 AST 的生成过程可能会有所差异。但总体上,AST 的生成是通过词法分析、语法分析和语义分析等步骤来构建具有结构化表示的代码树状结构。

3.1. 词法分析 — 分词

词法分析阶段主要负责将源代码拆分成词法单元,并为每个词法单元分配相应的标记,简单来说就是在源代码的基础上进行分词。

比如把 “我爱中国” 这段字符串根据某种规则拆解成 “我”“爱”“中国” 三部分,这个过程就叫做 分词,其中各个部分又被称为 词法单元 ,对于代码来说,需要将代码片段识别为关键字、标识符、操作符、字面量等部分,也就是分配标记。

我们可以这么理解,词法分析就是把你的代码从字符串类型转换成了数组,数组的元素就是代码里的单词(词法单元),并且标记了每个单词的类型。比如下面这段代码:

const a = 10;

词法分析器 从左往右逐个字符扫描分析 整个程序的字符串,当遇到不同的字符时,会驱使它迁移到不同的状态。在扫描字符的时候,遇到 c 字母,如果后面还有字符,将继续扫描,直到遇到空格,识别出 const,发现是一个关键字,将其生成词法单元:

{ type: 'Keyword', value: 'const' }

然后接着扫描,以此类推,生成 Token List

所以,这段程序会被分解成为下面这些词法单元:leta=10;,你可以在 这里 >> 查看词法分析结果:

[
  { type: 'Keyword',    value: 'const' },
  { type: 'Identifier', value: 'a'     },
  { type: 'Punctuator', value: '='     },
  { type: 'Numeric',    value: '10'    },
  { type: 'Punctuator', value: ';'     },
];

通过上面分解出来的词法分析结果,我们可以观察到,缺少一些比较关键的信息:

  1. 没有任何语法信息;

  2. 体现不了代码的执行顺序;

所以,需要进一步进行 语法分析

3.2. 语法分析 — 解析

语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST)。同时,验证语法,语法如果有错的话,将抛出语法错误。

词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会在读取所有的词法记号后再使用语法分析器来处理。通常情况下,每取得一个词法记号,就将其送入语法分析器进行分析。

js-token-ast.png

我们可以使用在线工具生成 AST:AST explorer >> 或者 Esprima.org >>,这里主要简单介绍下 AST explorer 的使用:

ast_explorer.png

比如这一段代码:

function square(n) {
  return n * n;
}

经过转化,输出 AST 结构如下:

ast_eg1.png

你会留意到 AST 的每一层都拥有相同的结构:

{
    type: "FunctionDeclaration",
    id: {...},
    params: [...],
    body: {...}
}
{
    type: "Identifier",
    name: ...
}
{
    type: "BinaryExpression",
    operator: ...,
    left: {...},
    right: {...}
}

提示:出于简化的目的移除了某些属性。

这样的每一层结构,被称为 节点,每一个节点都包含 type 属性,用于表示节点类型,比如:FunctionDeclarationIdentifierBinaryExpression 等等。除此之外,Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置,比如: startendloc

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 >>

语句是能够独立执行的基本单位,常见的语句类型有:

类型名称中文译名描述
IfStatementIf 控制流语句通常指 if (true) {} else {}
ForInStatementFor-in 循环语句通常指 for(let key in obj) {}
SwitchStatementSwitch 语句通常指 switch
WhileStatementWhile 循环语句通常指 while(true) {}
ForStatementFor 循环语句通常指 for(let i = 0; i < 10; i++) {}
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
BlockStatement块语句包裹在 {} 内的语句
ExpressionStatement表达式语句通常为调用一个函数,比如 console.log(1)

4.4. 声明「Declaration >>

声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个 变量、函数、classimportexport 等。

类型名称中文译名描述
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.png

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 的编译流程分为三步:ParseTransformGenerate,每一步都暴露了一些 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 节点进行增删改,遍历一个节点的过程是:EnterTraverse child nodeExit

语法形式如下:

require("@babel/traverse").default(ast, visitors)
  • ast:抽象语法树AST
  • visitors:访问者

示例代码:

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');
  },
});

最佳实践建议

  1. 优先使用类型级访问,避免全局访问带来的性能损耗。
  2. 复杂场景可使用 | 语法组合多个节点类型

参数 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
  1. 同步编译

    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']
    });
    
  2. 异步编译

    const { transformAsync, transformFileAsync, transformFromAstAsync } = require('@babel/core');
    
    // 异步编译示例
    async function compile() {
      const result = await transformAsync('class A {}', {
        presets: ['@babel/preset-env']
      });
      console.log(result.code);
    }
    
  3. 输出结果结构:

    所有编译方法都返回包含以下属性的对象:

    {
      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 以前(旧版本)
  • .babelrc
  • package.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”选项卡中找到编译前的源文件,方便开发者进行调试。

    但首先得确保你开发者工具的设置里的这一项是处于勾选状态:右键检查工具栏中选择更多(右上角三个竖着的小圆点)SettingSourcesEnable 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个:

ast_visitor.png

所以 Identifier() 方法会被调用四次:

Called!
Called!
Called!
Called!

3. Path(路径)

AST 通常会有许多节点,我们可以通过 path 对象表示节点之间的关联关系,所以,path 是表示两个节点之间连接的 对象。通过这个对象提供的属性方法,我们可以 操作 AST 语法树。

babel_path.png

上图中红色方框处表示对应节点的 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:获取容器的 key
  • path.container:获取路径的容器(包含所有同级节点的数组)
  • path.inList:判断路径是否有同级节点

2.2. 方法

  • path.toString():当前路径所对应的源代码;
  • path.isXXXXXX 为节点类型,可以判断是否符合节点类型。比如我们需要判断路径是否为 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 1global 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();
            }
          }
        }
      },
    },
  };
};