接触过 Babel 插件编写的话,可能有个感受,官方文档提供的介绍太少了,社区中关于如何写插件介绍的比较多,但对其中用到的工具的介绍却比较少。
为什么需要插件呢?
babel-parser 主要有两个功能:
- 识别各种语法,且默认情况下,并不开启所有语法的识别支持,这是出于性能考虑
- 解析源码字符串为抽象语法树,并不负责对源码的转译
在 Babel 的整体设计中,对于部分语法支持的开关、对抽象语法树的读取和修改等操作,均用插件配合完成。有些插件可以作为语法开关,有些插件可以解析修改抽象语法树以实现转译功能。
那么,本章就来攻克下 Babel 插件编写这一环吧。
编写插件的过程,实际上是对 AST 节点的遍历和操作。需要了解如何遍历节点、如何操作节点。
# 6.1 遍历节点
Babel 通过 babel-traverse 遍历 AST 节点,并在遍历的过程中,接收开发者传入的处理逻辑对 AST 节点进行增删改查。
# 6.1.1 babel-traverse
基本功能
babel-traverse 维护了 Babel 的节点状态,通过它,可以遍历 AST 节点,插件开发者可以在遍历过程中对节点进行替换、删除、添加等操作。
比如:
import * as parser from '@babel/parser'; import traverse from '@babel/traverse'; import generator from '@babel/generator'; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { enter(path) { if (path.node.type === 'Identifier' && path.node.name === 'n') { path.node.name = 'x'; } } }); const { code: newCode } = generator(ast); console.log(newCode);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21traverse接收一些参数,并处理后,AST 发生了变动。上述代码执行后,打印
newCode结果是:function square(x) { return x * x; }1
2
3参数
babel-traverse 对外暴露的方法
traverse定义在packages/babel-traverse/src/index.ts:function traverse( parent: t.Node, opts: TraverseOptions = {}, scope?: Scope, state?: any, parentPath?: NodePath, ) { ... }1
2
3
4
5
6
7
8
9各参数含义如下:
parent: 要遍历的节点对象opts: 遍历配置项参数
opts的类型描述TraverseOptions内容如下:type TraverseOptions<S = t.Node> = | { scope?: Scope; noScope?: boolean; denylist?: string[]; } | Visitor<S>;1
2
3
4
5
6
7也就是说,
opts的值可以是:{ scope, noScope, denyList }1也可以是一个 Visitor(访问者),下节将详细介绍 Visitor。
state: TODOTODO
parentPath: TODOTODO
# 6.1.2 Visitor
访问者模式
TODO
Visitor 对象
babel-traverse 接收一个 Visitor 对象,开发者通过该对象可以拦截 babel-traverse 对某个节点的访问时刻,并做处理。
Visitor 对象有哪些格式呢?babel-traverse 中定义的 Visitor 类型如下:
export type Visitor<S = {}> = VisitNodeObject<S, t.Node> & { [Type in t.Node["type"]]?: VisitNode<S, Extract<t.Node, { type: Type }>>; } & { [K in keyof t.Aliases]?: VisitNode<S, t.Aliases[K]>; } & { [K in keyof VirtualTypeAliases]?: VisitNode<S, VirtualTypeAliases[K]>; } & { [k: string]: VisitNode<S, t.Node>; }; export type VisitNode<S, P extends t.Node> = | VisitNodeFunction<S, P> | VisitNodeObject<S, P>; export type VisitNodeFunction<S, P extends t.Node> = ( this: S, path: NodePath<P>, state: S, ) => void; export interface VisitNodeObject<S, P extends t.Node> { enter?: VisitNodeFunction<S, P>; exit?: VisitNodeFunction<S, P>; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27可以看到,Visitor 可以有多种写法:
enter/exit 式写法
{ enter(...) { ... }, exit(...) { ... } }1
2
3
4
5
6
7
8比如,
babelTraverse(ast, { enter() {}, exit() {} })1
2
3
4遍历ast上的每个时,
enter/exit均会触发。有意思的是,
babel-core内部也实用了babel-traverse对目标代码对应的 AST 节点进行遍历,但它禁止这样写,比如:index.js:
const babel = require('babel-core'); const code = `const a = 1;` babel.transformSync(code, { plugins: [ './plugin.js' ] })1
2
3
4
5
6
7
8./plugin.js:module.exports = function(api) { return { visitor: { enter(path, state) { console.log(111) }, exit(path, state) { } } } };1
2
3
4
5
6
7
8
9
10
11
12babel-core 将这种写法视为非法:
cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.。节点名称 + 函数式写法
Identifier(...) { ... }1
2
3节点名称 + enter/exit 式写法
Identifier: { enter(...) { ... }, exit(...) { ... } }1
2
3
4
5
6
7
8
其中,节点名称可以是以下几种方式:
节点名称
定义在
babel-types/src/ast-types/generated/index.ts的Node字段。节点别名
定义在
babel-types/src/ast-types/generated/index.ts的Aliases字段,以及定义在babel-traverse/src/path/generated/virtual-types.ts的类型。其他名称
源码中是这样写的:
[k: string]: VisitNode<S, t.Node>;1可以是任何字符串,但很显然没有这么自由,具体可写的内容后文会介绍。
Visitor 的参数
对 Visitor 处理函数的封装
babel-traverse 内部对传入的插件处理函数做了一层封装,在文件
babel-traverse/src/visitors.ts中有这么一段代码:let newFn = fn; if (state) { newFn = function (path) { // 执行 visitor return fn.call(state, path, state); }; } if (wrapper) { newFn = wrapper(state.key, key, newFn); } // Override toString in case this function is printed, we want to print the wrapped function, same as we do in `wrapCheck` if (newFn !== fn) { newFn.toString = () => fn.toString(); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17其中,
fn是插件传入的处理方法,newFn是封装后的方法,有这么几个特点:fn的两个参数是path和statefn执行的时候,上下文是statenewFn重写了toString方法,当需要打印函数体时,可以还原fn,这是一个值得借鉴的细节
Visitor 的参数
上述代码中,Visitor 对象提供的处理函数
fn可接收 2 个参数:path、state,且this的值为state。path和state对象描述了当前被访问节点的各类信息,这两个参数携带的属性和方法非常多,下文另开一节对其详细介绍。
节点处理队列
插件开发者提供的 Visitor,在 babel-traverse 处理时,会针对每个 Visitor 的 key(节点名称),会有一个数组用于存储插件传入的处理方法,也就是说,针对一个节点,有一个处理队列。
# 6.1.3 path
path 对象是内部 NodePath 类的实例。
# path功能
path 上的 API 非常多,如下图所示:

根据上图,从来源看,path 作为 NodePath 类的实例,该对象上的 API 可以分为几类:
path对象自身的属性和方法path继承自NodePath类的原型上的属性和方法- 各功能模块对外暴露的属性和方法,被添加到
NodePath.prototype,便于path使用 - 各功能功能模块为
path对象直接设置的属性/方法
从功能上,path 有以下能力:
- 断言与校验,等同于 babel-types 的断言
- 操作祖先节点、家族节点
- 替换、转换、修改、删除节点
- 节点计算操作
- 获取节点上下文的信息
- 获取节点命名空间的信息
- 和注释节点相关的操作
- ...
# API
笔者将下文中部分 API 的调用链路进行了图解,重点描述:path.context、插件引入的方法执行链路等信息。

阅读建议:下文的 API 介绍可以参照上图一起了解
基本属性/方法
path.node: t.Node表示 AST 节点对象。关于节点对象的具体信息,章节"Babel节点集"中对各个节点有详细的介绍。
path.opts: any = nullpath.opts就是执行babelTraverse传入的遍历配置项。从上文的图解可以看到该属性的设置过程:babelTraverse(..., opts, ...) --> // ./context.ts context = new TraversalContext({ ..., opts }) context.opts = opts --> // ./path/context.ts path.opts = context.opts1
2
3
4
5
6
7
8
9
10
11
12path.skipKeys- babel-traverse/src/context.ts
path.parentPath父级
path对象。path.containerTODO
path.listKeypath.keypath.typepath.inListpath.getScope(scope: Scope): Scope获取当前节点对应的
scope作用域空间对象。path.traverse<T>(visitor: Visitor<T>, state: T): void遍历当前节点及其子节点。
path.buildCodeFrameError内部调用的是
path.hub.buildError:buildError(node, msg, Error = TypeError): Error { return new Error(msg); }1
2
3path.parent表示当前节点的父节点。
上下文信息:
path.contextpath.context: TraversalContextpath.context是TraversalContext类的实例,表示当前访问节点的上下文信息。文件babel/packages/babel-traverse/src/context.ts定义了TraversalContext类。在遍历节点队列的过程中,每访问一个节点,
path.context就会被设置为被访问节点对应的上下文context对象,上图有描述。一个较为迷惑的点是,
./context.ts和./path/context.ts二者有什么区别?./context.ts定义了TraversalContext类,该类的实例,作为属性path.context的值./path/context.ts对外暴露的各个方法和属性,可以直接被path对象访问,因为这些方法和属性被直接添加到NodePath.prototype上,path作为NodePath的实例是可以直接访问的
path.context.visit(node, key): boolean遍历
node[key]节点集合及其子节点,返回值表示是否结束节点遍历。该方法的执行,会调用 Babel 插件里注册的处理函数,分别在
enter/exit阶段执行,从源码和上图可以看到如下执行链路:// ./context.ts path.context.visit() --> // ./path/context.ts path.visit() --> // ./path/context.ts path.call() --> // ./path/context.ts fns队列依次执行,fns就是插件传入的处理方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17这是插件提供的处理函数的调用方式:
const ret = fn.call(this.state, this, this.state); if (ret && typeof ret === "object" && typeof ret.then === "function") { throw new Error( `You appear to be using a plugin with an async traversal visitor, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, you may need to upgrade ` + `your @babel/core version.`, ); } if (ret) { throw new Error(`Unexpected return value from visitor method ${fn}`); }1
2
3
4
5
6
7
8
9
10
11
12可以看到:
- 函数的
this指向path.state,两个参数分别指向path和path.state - Babel 还未支持异步的 Visitor 处理函数
- Visitor 处理函数不能反悔非空值
- 函数的
path.contexts: Array<TraversalContext>某种类型的节点队列在遍历过程中,
path对象维护了一个path.contexts数组,每当遍历一个节点时,path.contexts数组即会将当前被访问节点的上下文信息对象context推入该数组。如果整个队列遍历结束,则会清空path.contexts。
作用域信息:
path.scopepath.scope描述path.node节点所在的作用域的所有信息。path.scope对象对应的构造函数Scope,定义于文件babel/packages/babel-traverse/src/scope/index.ts,通过init/crawl等方法完成实例的初始化操作。path.scope.binding: Binding既然介绍作用域空间,肯定要涉及变量绑定等信息,babel-traverse 用单独的
babel/packages/babel-traverse/src/scope/binding.ts文件定义了绑定信息类Binding。在此初始化一个
Binding类的实例binding。path.scope.binding = new Binding(...);1以下是
path.scope.binding对象的一些属性和方法:path.scope.binding.identifier: t.Identifier指向标识符 AST 节点。
path.scope.binding.scope: Scope对
scope的反向引用:path.scope.binding.scope指向path.scope。path.scope.binding.path: NodePath对
path的反向引用:path.scope.binding.path指向path。path.scope.binding.kind: string绑定类型,值可以是:
"var" | "let" | "const" | "module" | "hoisted" | "param" | "local" | "unknown"。path.scope.binding.references: number表示
binding对象对应的变量被引用的次数。以下述代码为例:let key; for (let i of [ 1, 2, 3 ]) { key key }1
2
3
4
5变量
key的binding.references值是 2,因为for循环内部有 2 处对key的引用。path.scope.binding.referencePaths: Array<NodePath>它是一个数组,用于记录
path.node被引用的各个节点。path.scope.binding.referenced: boolean描述当前
path.node节点是否被引用到。该属性可用于"tree-shaking"功能,如果没有被引用到,则该节点可被删除。
path.scope.bindings该对象初始值为
Object.create(null),近似于{},但原型链查找链路只有一层(对象自身)。babel-traverse 在节点扫描过程中会为该对象不断添加当前
path.node节点作用域内的变量绑定信息,设置方式为:// this 就是 path.scope this.bindings[name] = new Binding({ identifier: id, scope: this, path: bindingPath, kind: kind, });1
2
3
4
5
6
7path.scope.uid: string唯一的 scope id。
babel-traverse 过程中,各个
path.scope具有唯一的uid,如"_id1",其中id1往往是当前变量的名称。path.scope.path: NodePath反向引用:
path.scope.path指向path。path.scope.blockTODO
path.scope.globalspath.scope的内部变量,用于当前节点所处的ProgramParent节点存储一些绑定信息。path.scope.uids: { [name: string]: boolean }TODO,待验证
初始值为
Object.create(null)。该对象记录了
path.scope作用域中涉及的所有节点的uid,其值类似:{ _id1: true, _id2: true, _id3: false }。path.scope.data: Objectpath.scope.data记录了 babel-traverse 运行过程中的一些数据信息,用于内部流转。path.scope.crawling: boolean标志位,表示
path.scope是否在初始化过程中,因为初始化中涉及对各类树的遍历操作,因此称为crawling。path.scope.parent指向父级
scope节点。path.scope.parentBlock指向父级
block。path.scope.hubTODO
遍历方法:
path.scope.traverse: Function该方法就是 babel-traverse 对外暴露的
traverse方法,用于遍历节点树。path.scope.generateUidIdentifier(name?: string): t.Identifier生成基于
name,且名称唯一的、新 Identifier 节点。path.scope.generateDeclaredUidIdentifier(name?: string): t.Identifier生成基于
name,且名称唯一的、新 Identifier 节点,不过其返回的是用path.scope.generateUidIdentifier生成的新节点的副本节点。path.scope.generateUid(name?: string): string基于
name生成新的唯一的uid。内部使用
do...while保证了对名称唯一性验证的持续进行,同时新的uid避免和以下场景冲突:this.hasLabel(uid): 和标记的关键词同名this.hasBinding(uid): 和绑定的变量同名this.hasGlobal(uid): 和全局变量同名this.hasReference(uid): 和被引用中的变量同名
path.scope.generateUidBasedOnNode(node: t.Node, defaultName?: string): string基于
node生成uid。方法内部通过对node的递归遍历,生成一个name数组。案例TODOpath.scope.generateUidIdentifierBasedOnNode(node: t.Node, defaultName?: string): t.Identifier生成既有基于
node生成的新uid的新Identifier节点。path.scope.isStaticTODO
path.scope.maybeGenerateMemoisedTODO
path.scope.checkBlockScopedCollisionsTODO
path.scope.renameTODO
path.scope.dumpTODO
path.scope.hasLabel(name: string): booleanpath.node是否含有标签类型的关键字。path.scope.getLabel(name: string): NodePath<t.LabeledStatement>获取
path.node携带的标签关键字节点。path.scope.registerLabel(path: NodePath<t.LabeledStatement>): void将标签关键字节点注册到当前
scope。path.scope.registerDeclaration(path: NodePath): void根据
path对应的节点类型,注册到各类map对象中。和
path.scope.binding.kind: string字段对应的("var" | "let" | "const" | "module" | "hoisted" | "param" | "local" | "unknown"),以下类型的节点会被注册:LabeledStatement: 写入this.labels对象FunctionDeclaration: 写入hoistedVariableDeclaration: 写入path.scope.bindings的var/let/const类型ClassDeclaration: 写入path.scope.bindings的let类型ImportDeclaration: 写入path.scope.bindings的module类型ExportDeclaration: 解构该节点,并递归注册- 无法识别的,注册为
unknown
path.scope.buildUndefinedNode(): t.UnaryExpression生成一个
undefined节点。path.scope.registerConstantViolationTODO
path.scope.registerBinding为
path.scope.bindings对象设置当前path.node节点对应作用域的所有绑定信息。path.scope.addGlobalTODO
path.scope.hasUid(name: string): boolean沿着
path.scope及以上的scope节点对象,判断是否含有uid为name的节点。path.scope.hasGlobal(name: string): boolean判断当前节点及各级父节点中,每个
scope.globals对象是否含有key为name的属性。path.scope.hasReference(name: string): boolean直接根据顶层作用域节点
ProgramParent得知是否含有uid为name的节点。path.scope.isPureTODO
path.scope.crawl初始化
path.scope实例时执行的方法,crawl意为"爬取",该方法有以下作用:- 初始化
path.scope的一系列属性:references/bindings/globals/uids/data - 遍历当前
path节点及其子节点,注册path.scope的各类属性
- 初始化
path.scope.getProgramParent(): Scope | null向上遍历
scope树,直到遇到类型是Program的scope节点。path.scope.getFunctionParent(): Scope | null向上遍历
scope树,直到遇到类型是Function的scope节点。path.scope.getBlockParent: Scope从当前节点到各级父节点,判断是否有块级作用域的存在。块级作用域对应的节点有:
BlockStatement/Loop/Program/Function/Switch。path.scope.getAllBindings: { [ key: string ]: Binding }从当前节点到各级父节点,遍历时收集每个
scope节点的scope.bindings信息。返回收集的所有绑定信息对象。path.scope.getAllBindingsOfKind(...kinds: string[]): any从当前节点到各级父节点,判断每个节点的类型是否匹配
kinds: string[]中的元素,如有匹配,则收集该scope节点的scope.bindings信息。返回收集的所有绑定信息对象。path.scope.bindingIdentifierEquals(name: string, node: t.Node): boolean从当前节点到各级父节点,获取
uid为name的标识符节点,并判断该节点和传入的node是否相同。path.scope.getBinding(name: string): Binding | undefined从当前节点到各级父节点,判断每个节点是否含有
uid为name的scope.binding对象,如有,则就近返回第一个匹配的binding对象。path.scope.getOwnBinding(name: string): Binding | undefined获取当前节点中,
uid为name的scope.binding对象。path.scope.getBindingIdentifier(name: string): t.Identifier | undefinedTODO PR从当前节点及各级父节点,获取索引
key为name的节点。path.scope.getOwnBindingIdentifier(name: string): t.Identifier | undefined从当前节点的
binding信息获取uid为name的节点。path.scope.hasOwnBinding(name: string): boolean判断当前节点中,是否含有
uid为name的scope.binding对象。path.scope.hasBinding(name: string, noGlobals?): boolean判断当前节点及向上一级的父节点是否有
uid为name的绑定节点信息。path.scope.parentHasBinding(name: string, noGlobals?)判断当前节点的向上一级的父节点,是否有
uid为name的绑定节点信息。path.scope.moveBindingTo(name: string, scope: Scope)移动当前命名空间中名称为
name的绑定信息到目标scope命名空间。path.scope.removeOwnBinding(name: string): void删除当前节点的
uid为name的绑定信息。path.scope.removeBinding(name: string): void删除当前节点及各级父节点的
uid为name的绑定信息。
家族节点(family)
path.getOpposite(this: NodePath): NodePath | null获取“对面”的兄弟节点,比如一个二元表达式含有左右两个节点,如
1 + 2,节点是left节点,该方法返回的就是right节点。源码为:
function getOpposite(this: NodePath): NodePath | null { if (this.key === "left") { return this.getSibling("right"); } else if (this.key === "right") { return this.getSibling("left"); } return null; }1
2
3
4
5
6
7
8path.getCompletionRecordsTODO
path.getSibling(this: NodePath, key: string | number): NodePath获取对应
key值的兄弟节点。path.getPrevSibling(this: NodePath): NodePath获取前一个兄弟节点。
path.getNextSibling(this: NodePath): NodePath获取下一个兄弟节点。
path.getAllNextSiblings(this: NodePath): NodePath[]获取后面所有的兄弟节点。
path.getAllPrevSiblings(this: NodePath): NodePath[]获取前面所有的兄弟节点。
path.get<T extends t.Node>(this: NodePath<T>, key: string, context?: true | TraversalContext ): NodePath | NodePath[]根据
key值获取目标节点。key的内容可以是"foo"或"foo.bar"这样的写法。path.getBindingIdentifiers( duplicates?: boolean ): Record<string, t.Identifier[] | t.Identifier>获取和
path.node节点绑定的标识符列表。该方法等同于t.getBindingIdentifiers()path.getOuterBindingIdentifiers(duplicates: true ): Record<string, t.Identifier[]>TODO
path.getBindingIdentifierPaths(this: NodePath, duplicates: boolean = false, outerOnly: boolean = false ): { [x: string]: NodePath;}TODO
path.getOuterBindingIdentifierPaths(this: NodePath, duplicates?: boolean ): { [x: string]: NodePath;}TODO
祖先节点(ancestry)
path.findParent(callback: (path: NodePath) => boolean ): NodePath | null从当前节点的父节点开始,递归向上查找目标祖先节点,直到
callback返回true,callback参数为递归向上过程的各级父节点。path.find(callback: (path: NodePath) => boolean ): NodePath | null从当前节点开始,递归向上查找目标祖先节点,直到
callback返回true,callback参数为递归向上过程的各级父节点。path.getFunctionParent(this: NodePath): NodePath<t.Function> | null这是基于
findParent的快捷方法,如果向上递归过程中,有一个祖先节点是函数,则停止查找并返回该节点。path.getStatementParent(this: NodePath): NodePath<t.Statement>从当前节点开始,递归向上查找类型为
Statement的祖先节点。path.getEarliestCommonAncestorFrom(this: NodePath, paths: Array<NodePath> ): NodePathTODO
path.getDeepestCommonAncestorFrom参数及返回值类型:
path.getDeepestCommonAncestorFrom(this: NodePath, paths: Array<NodePath>, filter?: (deepest: t.Node, i: number, ancestries: NodePath[][]) => NodePath): NodePath1TODO
path.getAncestry(this: NodePath): Array<NodePath>获取包含当前节点的所有祖先节点的节点列表。
path.isAncestor(this: NodePath, maybeDescendant: NodePath): booleanTODO
path.isDescendant(this: NodePath, maybeAncestor: NodePath): boolean判断
maybeAncestor节点是否是node的祖先节点,内部利用findParent方法。path.inType(this: NodePath, ...candidateTypes: string[]): booleanTODO
推断相关: inference
path.getTypeAnnotation(): t.FlowType获取当前节点的类型推断。
path.isBaseType(baseName: string, soft?: boolean): boolean是否是基本类型,包括:
string / number / boolean / any / mixed / empty / void。值得注意的是,该方法接收一个
soft: boolean属性,表示是否是宽松式地检查,如果值为false,则会抛出错误。path.couldBeBaseType(name: string): boolean判断当前节点是否是基础类型,包括:
string / number / boolean / any / mixed / empty / void。TODO
path.baseTypeStrictlyMatches(rightArg: NodePath): booleanTODO
path.isGenericType(genericName: string): booleanTODO
计算相关: evaluation
path.evaluateTruthy(this: NodePath): boolean | undefined计算当前表达式节点的计算结果是否可以转为
true或false,该方法可能有如下返回值:true: 确定该表达式的计算结果可以转为truefalse: 确定该表达式的计算结果可以转为falseundefined: 不确定该表达式的计算结果是否可转为true还是false
对于该方法的返回值
returnedValue,要特别注意要使用returnedValue === false判断其是否为false。path.evaluate(this: NodePath): { confident: boolean; value: any; deopt?: NodePath; }计算当前表达式节点的值的相关信息。
当然这也是尝试计算,所以该方法的返回值结构为:
{ confident, // 表示是否确定能获取表达式的值,类型为 boolean value, // 表达式的值,默认为 undefined deopt // TODO }1
2
3
4
5源码的注释里介绍了几种场景:
t.evaluate(parse("5 + 5")) // { confident: true, value: 10 } t.evaluate(parse("!true")) // { confident: true, value: false } t.evaluate(parse("foo + foo")) // { confident: false, value: undefined, deopt: NodePath }1
2
3
introspection
path.matchesPattern(this: NodePath, pattern: string, allowPartial?: boolean ): boolean判断当前节点是否匹配传入的
pattern模板字符。比如pattern = 'React.createClass'匹配的AST节点对象是React.createClass和React["createClass"]。方法内部依赖 babel-types:
t.matchesPattern(this.node, pattern, allowPartial)。path.has(this: NodePath, key: string): boolean判断
node节点对象的node[key]是否有值。这里额外判断了
node[key]为数组时,是否为空数组的情况。path.is(this: NodePath, key: string): boolean等同于
path.has。path.isnt(this: NodePath, key: string): boolean等同于
!path.is(this: NodePath, key: string): boolean。path.isStatic(this: NodePath): booleanfunction isStatic(this: NodePath): boolean { return this.scope.isStatic(this.node); }1
2
3判断当前
node节点所在的作用域是否是静态的。TODOpath.equals(this: NodePath, key: string, value): boolean检查当前
node的 key 值是否严格等于 value。path.isNodeType(this: NodePath, type: string): booleanfunction isNodeType(this: NodePath, type: string): boolean { return t.isType(this.type, type); }1
2
3判断当前节点类型
node.type是否是目标类型type。path.canHaveVariableDeclarationOrExpressionfunction canHaveVariableDeclarationOrExpression(this: NodePath) { return ( (this.key === "init" || this.key === "left") && this.parentPath.isFor() ); }1
2
3
4
5判断当前节点是下述
for循环中的变量:for (KEY in right); for (KEY;;);1
2path.canSwapBetweenExpressionAndStatement判断一个节点是否可以在表达式与语句之间相互转化,针对的是这种写法:
() => true () => { return true; }1
2
3path.isCompletionRecordTODO
path.isStatementOrBlockTODO
path.referencesImportTODO
path.getSourceTODO
path.willIMaybeExecuteBeforeTODO
path.resolveTODO
path.isConstantExpressionTODO
path.isInStrictMode判断当前节点是否位于严格模式的上下文中。
上下文相关(context)
path.call(this: NodePath, key: string): boolean- 执行 Visitor 传入的相关节点处理方法
- 返回 true 时:TODO
- 返回 false 时:TODO
path.isDenylisted(this: NodePath): booleanbabelTraverse(node, opts)中,opts有配置项denyList: Array<string>,配置不访问的节点。path.visitpath.skippath.skipKeypath.stoppath.setScopepath.setContextpath.resyncpath.popContextpath.pushContextpath.setuppath.setKeypath.requeue
替换节点: replacements
path.replaceWithMultiple<Nodes extends Array<t.Node>>(nodes: Nodes): NodePath[]用多个节点替换掉当前节点,并返回替换后的
path节点列表。path.replaceWithSourceString(this: NodePath, replacement): void用字符串形式的代码,替换目标节点
node。内部实现上,会使用
babel-parser的parse方法将代码字符串解析为 AST 节点,然后通过replaceWith方法完成节点替换。解析代码字符串失败的话,会抛出错误,描述为
BABEL_REPLACE_SOURCE_ERROR错误,并用babel-code-frame描述出错的代码位置信息。path.replaceWith(this: NodePath, replacement: t.Node | NodePath)TODO
path.replaceExpressionWithStatements(this: NodePath, nodes: Array<t.Statement>)TODO
path.replaceInline(this: NodePath, nodes: t.Node | Array<t.Node>)TODO
转换节点: conversion
path.toComputedKey(this: NodePath)path.ensureBlockTODO
path.arrowFunctionToShadowedpath.unwrapFunctionEnvironmentpath.arrowFunctionToExpression将箭头函数转为 ES5 里的函数表达式。
参数:
allowInsertArrow: TODOspecCompliant: TODOnoNewArrows: TODO
删除节点: removal
path.remove
修改节点: modification
path.insertBeforepath.insertAfterpath.updateSiblingKeyspath.unshiftContainerpath.pushContainerpath.hoist
注释相关: comments
path.shareCommentsWithSiblingspath.addComment添加一条注释。
path.addComments添加多条注释。
校验节点: validate
文件
packages/babel-traverse/src/path/index.ts中,为NodePath.prototype上添加了校验方法,这些方法全部来自 babel-types。使用方式为
path.is{NodeName},如path.isFunctionStatement(...)。
# 6.1.4 state
babel-traverse 负责调用插件传入的处理方法,其本身是支持传入一个初始化的state的,开发者可以根据自己需要传入一个初始化的 state 值,traverse 方法的定义如下:
function traverse(
parent: t.Node,
opts: TraverseOptions = {},
scope?: Scope,
state?: any,
parentPath?: NodePath,
) {
...
}
2
3
4
5
6
7
8
9
开发者实际使用的时候,一般不需要独立使用 babel-traverse,而是根据 babel-core 间接调用 babel-traverse,babel-core 在内部为 state 对象提供了一些属性、方法,便于开发者使用,如下图所示:

scope - index - crawl
在开发者没有设置 state 的情况下,内置遍历过程提供的 state 对象是 babel-core/src/transformation/plugin-pass.ts 定义的 PluginPass 实例。
state 实例本身内容并不复杂,其有如下属性或方法:
state.keystate.cwdstate.filenamestate.filestate.file对象是File类的实例。File类定义自文件babel/packages/babel-core/src/transformation/file/file.ts。state.file.availableHelperstate.file.addHelperstate.file.addImportstate.file.buildCodeFrameError
# 6.2 节点操作: babel-types
Babel 使用 babel-types 进行节点操作。babel-types 对外暴露出的 API,总体来说,可以分为对节点的“读操作”和“写操作”。
- 读节点
- 节点校验
- 查找节点
- 节点遍历
- 写节点
- 创建节点
- 修改节点
- 转换节点
- 操作注释
- 复制节点
babel-types 也提供了对 TypeScript 和 Flow 的 API 支持
下文中,用 t 代表 babel-types对外暴露的对象。
# 6.2.1 读节点
读操作api分布:

查找节点
t.getBindingIdentifiers参数与返回值类型
t.getBindingIdentifiers(node: t.Node, duplicates?: boolean, outerOnly?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>1TODO
t.getOuterBindingIdentifiers参数与返回值类型
t.getOuterBindingIdentifiers(node: t.Node, duplicates?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>1TODO
校验节点
t.is参数与返回值类型
// 较源码做了简化 t.is(type: string, node: t.Node | null | undefined, opts?: undefined ): boolean1
2功能
判断给定的节点
node的类型是否是type的值。
t.is{NodeName}t.isBinding(node: t.Node, parent: t.Node, grandparent?: t.Node): booleanTODO
t.isBlockScoped(node: t.Node): boolean判断
node节点是否应用于块级作用域。满足条件的节点包括:函数声明(
FunctionDeclaration)、类声明(ClassDeclaration)、let/const声明的变量、TODOt.isImmutable(node: t.Node): booleanTODO
t.isLet(node: t.Node): booleanfunction isLet(node: t.Node): boolean { return ( isVariableDeclaration(node) && (node.kind !== "var" || node[BLOCK_SCOPED_SYMBOL]) ); }1
2
3
4
5
6t.isNode(node: any): node is t.Nodefunction isNode(node: any): node is t.Node { return !!(node && VISITOR_KEYS[node.type]); }1
2
3是否是
node节点的条件:节点存在且是合法的节点类型。t.isNodesEquivalentisNodesEquivalent(a, b): boolean判断两个节点是否相等。
t.isPlaceholderType(placeholderType: string, targetType: string): booleanTODO
t.isReferenced(node: t.Node, parent: t.Node, grandparent?: t.Node): booleanTODO
t.isScope(node: t.Node, parent: t.Node): booleanTODO
t.isSpecifierDefault(specifier: t.ModuleSpecifier): booleanTODO
t.isType(nodeType: string | null | undefined, targetType: string): booleanTODO
t.isValidES3Identifier(name: string): booleanTODO
t.isValidIdentifier(name: string, reserved: boolean = true): booleanTODO
t.isVar(node: t.Node): booleanTODO
t.matchesPattern(member: t.Node | null | undefined, match: string | string[], allowPartial?: boolean): booleanTODO
t.validate(node: t.Node | undefined | null, key: string, val: any): voidTODO
t.buildMatchMemberExpression(match: string, allowPartial?: boolean)TODO
遍历节点
t.traverse<T>(node: t.Node, handlers: TraversalHandler<T> | TraversalHandlers<T>, state?: T): voidTODO
t.traverseFast(node: t.Node | null | undefined, enter: (node: t.Node, opts?: any) => void, opts?: any): voidTODO
# 6.2.2 写节点
写操作api分布:

创建节点
t.{节点名称}api格式
babel-types 创建节点的 API 以
t.{节点名称}格式命名,其中节点名称既支持大写开头,也支持小写开头。如创建
Variance的话,可以是:t.Variance或t.variance。
创建节点的参数
创建节点的参数由节点定义决定。
例如,创建一个
for循环语句(ForStatement):for (let i = 0; i < 3; i++) { }1
2
3babel-types 中的 API 是
t.forStatement(...)那么,使用者如何知道
t.forStatement方法所需的参数呢?babel-types 创建 API 的参数可参考
babel-types/src/definitions/目录下各文件对节点的定义的builder和fields字段。ForStatement节点的定义在babel-types/src/definitions/core.ts文件,定义内容:defineType("ForStatement", { visitor: ["init", "test", "update", "body"], aliases: ["Scopable", "Statement", "For", "BlockParent", "Loop"], fields: { init: { validate: assertNodeType("VariableDeclaration", "Expression"), optional: true, }, test: { validate: assertNodeType("Expression"), optional: true, }, update: { validate: assertNodeType("Expression"), optional: true, }, body: { validate: assertNodeType("Statement"), }, }, });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21builder字段提供参数名称及顺序,field字段提供每个字段的格式校验规则。案例
从
ForStatement节点的定义可以看到,t.ForStatement方法的参数如下:init: 初始化节点就是例子中的
let i = 0。可以是变量声明(
VariableDeclaration)、表达式(Expression)。test: 条件语句节点就是例子中的
i < 3。需要是表达式(
Expression)。update: 更新语句节点就是例子中的
i++。需要是表达式(
Expression)。body: 循环体就是例子中的循环体部分。
综上,目标 for 循环语句:
for (let i = 0; i < 3; i++>) { }1
2
3创建该节点的方法为:
TODO
const forStatement = t.forStatement( );1
2
3
部分 AST 节点没有对应的创建节点 API
babel-types 中,除了个别节点,其余节点均有对应的创建节点 API。
比如
Function节点,babel-types没有提供functionAPI 以创建Function节点,一方面是因为function本身就是 ECMAScript 的关键字,另一方面,函数要么以函数声明(FunctionDeclaration)出现,要么以函数表达式(FunctionExpression)出现,没有必要出现一个Function。笔者整理了一些没有没有对应的创建 API 的节点,如下:
FunctionStatementDeclarationExpressionObjectMemberUnaryOperatorUpdateOperatorBinaryOperatorLogicalOperatorPatternClassClassPrivateMethodStaticBlockModuleDeclarationModuleSpecifierImportAttribute
修改节点
t.appendToMemberExpressiont.inheritst.prependToMemberExpressiont.removePropertiest.removePropertiesDeept.removeTypeDuplicates
转换节点
t.ensureBlockt.toBindingIdentifierNamet.toBlockt.toComputedKeyt.toExpressiont.toIdentifiert.toKeyAliast.toSequenceExpressiont.toStatementt.valueToNode
操作注释
t.addCommentt.addCommentst.inheritInnerCommentst.inheritLeadingCommentst.inheritsCommentst.inheritTrailingCommentst.removeComments
复制节点
t.cloneNodet.clonet.cloneDeept.cloneDeepWithoutLoct.cloneWithoutLoc