AST节点
该章节只介绍常见的ast节点函数,如何通过这些函数创建对应的ast节点。同时简要说明了节点的。
AST常见节点
booleanLiteral
含义:
布尔字面量
JS代码示例:
true // 这就是一个布尔字面量
这里展示如何通过babel/types如何实现一个布尔字面量
babel代码示例:
const t = require("@babel/types");
// 创建布尔字面量 true 的 AST 节点
const trueLiteral = t.booleanLiteral(true);
// 创建布尔字面量 false 的 AST 节点
const falseLiteral = t.booleanLiteral(false);
// trueLiteral/falseLiteral 就是布尔字面量true/false对应的babel节点,将该节点添加ast对应的位置中就可实现在代码中对应的位置添加true/false这个字面量
// 一般情况下这种最基本类型字面量都是ast节点树的最小单元,即最基本单元,比表达式(expression)更基础
nullLiteral
含义:
null字面量,通过该方法创建一个null字面量节点
JS代码示例:
null
babel代码示例:
const t = require("@babel/types");
// 创建null字面量 null 的 AST 节点
const nullLiteral = t.nullLiteral();
// 这样我们就创建了一个null字面量节点,然后将这个节点添加到ast树的对应位置,那么就可以实现在代码的指定位置添加null这个字面量
stringLiteral
含义:
字符串字面量,通过该函数创建一个字符串字面量节点
JS表达式:
'hello'
babel代码示例:
const t = require("@babel/types");
// 创建字符串字面量的 AST 节点
const stringLiteral = t.stringLiteral('hello');
// 将该节点添加ast对应的位置中就可实现在代码中对应的代码
arrayExpression
含义:
数组表达式,因为是
JS代码示例:
[1,2,3] // 这就是一个数组字面量(表达式),在babel解析的时候它就是一个完整的arrayExpression类型
这里展示如何通过babel/types如何实现一个数组字面量
babel代码示例:
const t = require("@babel/types")
// 创建数组字面量节点
const arrayLiteral = t.arrayExpression([
t.numericLiteral(1),
t.numericLiteral(2),
t.numericLiteral(3)
]);
// arrayLiteral就是数组字面量[1,2,3]对应的babel节点,将该节点添加ast对应的位置中就可实现在代码中对应的位置添加[1,2,3]这个数组
arrowFunctionExpression
含义:
箭头函数表达式,可以通过该节点类型创建一个箭头函数
JS代码示例:
(a,b) => a + b // 这是一个箭头函数表达式
这里展示如何通过babel/types如何实现一个箭头函数表达式
babel代码示例:
const t = require("@babel/types");
// 创建箭头函数参数
const params = [t.identifier("a"), t.identifier("b")];
// 创建箭头函数体
const body = t.blockStatement([
t.returnStatement(
t.binaryExpression("+", t.identifier("a"), t.identifier("b"))
)
]);
// 创建箭头函数表达式
const arrowFunction = t.arrowFunctionExpression(params, body)
// arrowFunction 就是箭头函数(a,b) => a + b对应的babel节点,将该节点添加ast对应的位置中就可实现在代码中对应的位置添加(a,b) => a + b这个箭头函数
classDeclaration
含义:
类声明节点,通过该节点可以创建一个完整的类
JS代码示例:
class Personal {
name = 'xz'
getAge(){
return 18
}
}// 这是一个简单的Personal类的声明
babel代码示例:
const t = require("@babel/types")
// 创建类属性节点
const nameProperty = t.classProperty( //通过classProperty来实现类属性节点
t.identifier("name"), // 类属性的属性名
t.stringLiteral("xz") // 类属性的属性值
)
// 创建类方法节点
const getAgeMethod = t.classMethod( // 通过classMethod来实现类方法节点
"method", // 这个参数表示方法的类型,可以是 "method"、"get" 或 "set" 之一。"method":普通方法 "get":getter 方法 "set":setter 方法
t.identifier("getAge"), // 类方法的方法名
[], // 类方法的形参,因为这里我们没有指定,就默认为空数组
t.blockStatement([ // 类方法的方法体部分,传入一个数组,每一个元素都是一个可执行语句节点
t.returnStatement(t.numericLiteral(18))
])
)
// 创建类声明节点
const personalClass = t.classDeclaration(// 通过classDeclaration来声明一个类
t.identifier("Personal"),// 类名
null,// 这个参数表示类的父类(super class),通常是一个 Identifier 节点或 null。如果没有父类,则传入 null
t.classBody([nameProperty, getAgeMethod]),// 这是一个 ClassBody 节点,包含了类的属性和方法。通常是一个由 ClassProperty 和 ClassMethod 节点组成的数组。
[] // 这是一个装饰器(decorator)数组,用于指定应用于类的装饰器。通常是一个装饰器节点的数组。
)
// 这样我就通过babel创建一个完整的类的ast节点,然后将这个节点添加到ast树的对应位置
注意:
需要注意classDeclaration和classExpression的区别。前者是类声明,类表达式,他们的不同主要体现在代码实现上
类声明(classDeclaration):
class MyClass {
// 类的定义
}
类表达式(classExpression):
const MyClass = class {
// 类的定义
}
一般情况下在babel中主要创建的是类声明的节点
functionDeclaration
含义:
函数声明,通过该方法可以创建一个函数声明节点
JS代码示例:
function myFunction(param) {
return param + 1;
}
babel代码示例:
const t = require('@babel/types');
// 创建一个函数声明节点
const funcId = t.identifier('myFunction');
const paramId = t.identifier('param');
const functionNode = t.functionDeclaration(
funcId, // 函数名称
[paramId], // 参数列表
t.blockStatement([t.returnStatement(t.binaryExpression('+', paramId, t.numericLiteral(1)))]) // 函数体内容
);
// functionNode就是一个声明函数节点
functionExpression
含义:
函数表达式,通过该方法可以实现一个函数表达式节点
JS代码示例:
const myFunction = function myFunction(param) { // 这就是一个函数表达式节点
return param + 1;
};
babel代码示例:
const functionNode = t.functionExpression( // 配合variableDeclaration,创建一个函数表达式节点
funcId, // 函数名称
[paramId], // 参数列表
t.blockStatement([t.returnStatement(t.binaryExpression('+', paramId, t.numericLiteral(1)))]) // 函数体
);
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(funcId, functionNode)
]);
注意:
1、会发现functionExpression和functionDeclaration方法的传参基本像同,但是在创建不同的函数节点的时候,需要使用对应的函数
objectExpression
含义:
对象表达式,通过该方法可以创建一个对象节点
JS代码示例:
const person = {
name: 'Alice',
age: 30,
greet() {}
}
babel代码示例:
const t = require('@babel/types');
// 创建对象属性节点
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
// 创建空的 greet 方法节点
const greetMethod = t.objectMethod(
'method', // 普通方法
t.identifier('greet'), // 方法名
[], // 形参
t.blockStatement([]) // 函数体内容
); // objectMethod创建方法节点 es6方法格式
// 创建对象字面量节点
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
// 创建 const 声明语句节点
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
]);
assignmentExpression
含义:
赋值表达式,可以通过该节点类型创建一个赋值表达式
JS代码示例:
x = y + 1 // 这是一个赋值表达式
这里展示如何通过babel/types如何实赋值表达式
babel代码示例:
const t = require("@babel/types");
// 创建左侧的标识符节点
const left = t.identifier("x");
// 创建右侧的表达式节点
const right = t.binaryExpression("+", t.identifier("y"), t.numericLiteral(1));
// 创建赋值表达式节点
const assignmentExpression = t.assignmentExpression("=", left, right);
注意:
这里我们需要把声明赋值语句和普通的赋值语句区分开,声明赋值语句最后是通过variableDeclaration节点来实现的。而assignmentExpression是一个单纯的赋值操作。
例如 let name = 'dd' , name = 'cc'。前者是声明赋值,使用variableDeclaration实现,后者是使用assignmentExpression实现
binaryExpression
含义:
二元表达式,可以使用该节点创建一个二元表达式节点
js代码示例:
1 + 1 // 这是一个加法表达式
babel代码示例:
const t = require("@babel/types");
// 创建左侧的表达式节点
const left = t.numericLiteral(1);
// 创建右侧的表达式节点
const right = t.numericLiteral(1);
// 创建二元表达式节点
const binaryExpression = t.binaryExpression("+", left, right);
// binaryExpression 就是加法表达式1 + 1对应的babel节点,将该节点添加ast对应的位置中就可实现在代码中对应的位置添加1 + 1加法表达式
在 js 中,二元表达式是一种由两个操作数和一个运算符组成的表达式。以下是常见的 JavaScript 二元表达式:
算术运算符:
加法:+
减法:-
乘法:*
除法:/
取余:%
指数运算:**
关系运算符:
相等:==
不相等:!=
全等:===
不全等:!==
大于:>
小于:<
大于等于:>=
小于等于:<=
逻辑运算符:
逻辑与:&&
逻辑或:||
位运算符:
按位与:&
按位或:|
按位异或:^
左移:<<
有符号右移:>>
无符号右移:>>>
其他运算符:(因为其他运算符中的一些操作babel单独拎出来作为一个节点类型,因此二元运算符不包括其他运算符中的某些运算符)
赋值运算符:=
复合赋值运算符(如 +=、-=、*= 等)
实例关系运算符:instanceof
属性访问运算符:.
conditionalExpression
含义:
条件表达式,通过该方法创建一个条件表达式节点
js代码示例:
true ? 18 : 24 // 这是一个条件表达式
babel代码示例:
const t = require('@babel/types');
// 创建条件表达式的三部分
const condition = t.booleanLiteral(true); // test: 这是条件表达式中的条件部分,通常是一个逻辑表达式的节点,表示要进行判断的条件。
const trueExpression = t.numericLiteral(18);// consequent: 这是条件为真时返回的表达式,通常是一个表示条件为真时的操作的节点
const falseExpression = t.numericLiteral(24);// alternate: 这是条件为假时返回的表达式,通常是一个表示条件为假时的操作的节点。
// 创建条件表达式节点
const conditionalExpr = t.conditionalExpression(condition, trueExpression, falseExpression);
// conditionalExpr 就是我们创建的true ? 18 : 24条件表达式节点
arrayPattern
含义:
是js中表示解构赋值中的数组模式的一种AST节点类型,如果代码中包含数组解构赋值模式,对应的AST节点类型就是ArrayPattern
JS代码示例:
const [a,b,c] = [1,2,3] // 这是解构赋值表达式
这里展示如何通过babel/types如何实现数组的解构赋值
babel代码示例:
const t = require("@babel/types");
// 创建数组解构赋值语句的左侧模式
const arrayPattern = t.arrayPattern([
t.identifier("a"),
t.identifier("b"),
t.identifier("c")
]);
// 创建数组解构赋值语句的右侧表达式
const arrayExpression = t.arrayExpression([
t.numericLiteral(1),
t.numericLiteral(2),
t.numericLiteral(3)
]);
// 创建变量声明
const variableDeclaration = t.variableDeclaration("const", [
t.variableDeclarator(arrayPattern, arrayExpression)
]);
// 这就是const [a,b,c] = [1,2,3]解构赋值语句对应的babel节点,将该节点添加ast对应的位置中就可实现在代码中对应的位置添加const [a,b,c] = [1,2,3]这个解构赋值语句
decorator
含义:
装饰器节点,通过该方法可以创建一个装饰器表达式节点
// JS代码示例:
@preClass() // 这就是一个装饰器,该装饰器和Personal类绑定
class Personal {
}
babel代码示例:
const t = require('@babel/types');
// 创建 @preClass 装饰器节点
const preClassDecorator = t.decorator( // 通过decorator方法创建一个装饰器节点
t.callExpression(
t.identifier('preClass'), // 装饰器名称
[] // 传入到装饰器中的参数,因为我们没有向装饰器中传入参数,因此这里为空数组
)
);
// preClassDecorator就是一个装饰器节点,可以配合着类节点使用
// 创建一个类声明节点,并将 @preClass 装饰器应用于该类
const classDeclaration = t.classDeclaration(
t.identifier('MyClass'),
null,
t.classBody([]),
[preClassDecorator]
);
directive
含义:
指令注释,通过该方法可以创建一个指令注释节点
JS代码示例:
"use strict" // 这个就是指令注释
function personal {
}
babel代码示例:
const t = require('@babel/types');
// 创建指令注释节点
const directive = t.directive( // 通过directive创建一个指令注释节点
t.directiveLiteral('use strict') // 指令注释内容
);
注意:
在JS中,指令注释(Directive Comment)是一种特殊类型的注释,用于向 JavaScript 引擎传达特定指令或信息。指令注释通常以 "use strict"; 开头,其中 "use strict" 是最常见的指令注释之一。
除了 "use strict"; 指令注释外,还有其他一些指令注释可以用于指示不同的行为或配置,例如:
1、"use asm";: 告诉 JavaScript 引擎将代码编译为 asm.js 格式,用于优化性能。
2、"no-implicit-globals";: 禁止隐式声明全局变量,需要显式声明所有变量。
3、"eslint-disable";: 用于禁用 ESLint 检查器对特定代码段的检查。
这些指令注释可以根据需要在 JavaScript 代码中使用,以便向 JavaScript 引擎传达特定的指令或配置。在实际开发中,指令注释可以帮助开发人员控制代码的行为和性能。
objectPattern
含义:
对象的解构赋值,通过该函数创建一个对象解构赋值节点
JS代码示例:
const { name, age } = personal
babel代码示例:
const t = require('@babel/types');
// 创建对象解构赋值的左侧节点:{ name, age }
const objectPattern = t.objectPattern([ // 通过objectPattern创建对象的解构赋值节点
t.objectProperty(t.identifier('name'), t.identifier('name'), false, true),
t.objectProperty(t.identifier('age'), t.identifier('age'), false, true)
]);
// 创建 const 声明语句节点
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(objectPattern, t.identifier('person'))
]);
注意:
1、如果想要实现 const { name,age }这种es6写法的结构赋值,需要在t.objectProperty传入第三和第四个参数,分别为false和true
2、请和具名导入的{ xxx, xxx }区分开,那个不需要后面传入false true
memberExpression
含义:
成员表达式(或者说调用表达式),通过该函数创建一个成员表达式
JS代码示例:
obj.name // 调用表达式
babel代码示例:
const t = require('@babel/types');
// 创建成员表达式节点:obj.name
const memberExpression = t.memberExpression(
t.identifier('obj'), // 对象
t.identifier('name') // 属性
);
newExpression
含义:
new 关键字,通过该方法创建一个new表达式节点
JS代码示例:
new Personal()
babel代码示例:
const t = require('@babel/types');
// 创建 new 表达式节点:new Personal()
const newExpression = t.newExpression(
t.identifier('Personal'), // 构造函数
[] // 构造函数的参数
);
optionalMemberExpression
含义:
可选链成员表达式,通过该方法可以创建一个可选链成员表达式节点
JS代码示例:
object?.name // 如果object不为undefined或者null则访问name方法
babel代码示例:
const t = require('@babel/types');
// 创建可选链成员表达式节点:object?.name
const optionalMemberExpression = t.optionalMemberExpression(
t.identifier('object'), // 对象名
t.identifier('name'), // 属性名,
false,
true
);
注意:
1、optionalMemberExpression一共有四个参数,使用时必须传入四个参数
2、第三个参数主要是控制调用方式,为true则为object[name],为false则为object.name调用。默认传false
3、第四个参数主要是控制是否为可选调用,为true则为object?.name,为false则是普通调用即object.name
optionalCallExpression
含义:
可选调用表达式,通过该方法可以实现一个可选成员调用表达式
JS代码示例
obj.method?.()
babel代码示例:
const t = require('@babel/types');
// 创建可选链调用表达式节点:obj.method?.()
const optionalCallExpression = t.optionalCallExpression(
t.memberExpression(t.identifier('obj'), t.identifier('method')), // 成员表达式
[], // 方法参数
true // optional:boolean(必填)
);
含义:
1、如果我们直接obj.method()调用,可能method方法为undefined,如果直接调用就会报错,通过obj.method?.()这种就不会报错
thisExpression
含义:
this表达式,通过该方法可以创建一个this表达式节点
JS代码示例:
this.getName() // 通过this调用
babel代码示例:
const t = require('@babel/types');
// 创建 this 表达式节点
const thisExpr = t.thisExpression();
// 创建方法调用表达式节点:this.getName()
const callExpr = t.callExpression(
t.memberExpression(thisExpr, t.identifier('getName')),
[]
);
注意:
1、一般情况下this不会单独出现,会配合其他节点存在
callExpression
1、函数调用表达式
js代码示例
personal.name()
babel代码示例:
const t = require('@babel/types');
// 创建方法调用表达式节点:console.log(personal.name)
const logMemberExpression = t.callExpression(
t.memberExpression(t.identifier('console'), t.identifier('log')),
[t.memberExpression(t.identifier('personal'),t.identifier('name'))]
)
导入
importDeclaration
含义:
导入声明语句,可以通过该方法实现一个导入语句节点
JS代码示例:
<无>
babel代码示例:
// 创建 importDeclaration 节点
const importDeclaration = t.importDeclaration(
[importSpecifier],
t.stringLiteral('./personal.js')
);
注意:
1、因为默认导入节点、具名导入节点、命名空间导入节点都属于导入节点类型(importDeclaration),因此我们只要知道importDeclaration是一个导入节点即可,至于具体是什么类型的导入节点需要看传给该函数第一个参数的数组中包含的是什么具体到导入类型节点。传入的是具名类型节点,那它就是具名导入类型节点。
2、importDeclaration的作用就是生成一个导入节点,而具体是什么类型的节点需要看传入的参数
3、我们可以认为importDeclaration实现的是 import xxx from 'yyy' 这个结构,而至于xxx是一个什么类型需要看传入的specifier(标志位)是什么类型的标志
importSpecifier
含义:
具名导入标志,通过该方法可以创建一个具名导入标志,配和importDeclaration生成一个具名导入节点。Specifier翻译为标志。
JS代码示例:
import { name } from './personal.js' // 这是一个具名导入语句
babel代码示例:
const t = require('@babel/types');
// 创建 具名导入标志
const importSpecifier = t.importSpecifier(
t.identifier('name'),
t.identifier('name'),
);
// 创建 具名导入节点
const importDeclaration = t.importDeclaration(
[importSpecifier],
t.stringLiteral('./personal.js')
);
// importDeclaration 就是JS代码示例中具名导入语句的节点
注意:
1、在importSpecifier中,通过传入两个相同的t.identifier('name')就可以实现 { name }这种es6对象属性的写法
2、关于t.importSpecifier方法的两个参数
第一个参数 (imported):表示要导入的标识符的名称。这个参数应该是一个 Identifier 节点,代表被导入模块中的标识符名称。
第二个参数 (local):表示将被引入的本地变量名。这个参数也应该是一个 Identifier 节点,代表在当前模块中使用的这个标识符的名称。
3、如果我们将代码改为const importSpecifierNode = t.importSpecifier(t.identifier('name'), t.identifier('name1'));则最后会变为:
import { name1 as name } from "./personal.js"; 相当于重命名了
importDefaultSpecifier
含义:
默认导入标志,通过该方法可以实现一个默认导入标志,配和importDeclaration生成一个默认导入节点。Specifier翻译为标志。
JS代码示例:
import name from './personal.js' // 这是默认导入语句
babel代码示例:
const t = require('@babel/types');
// 创建 默认导入标志
const importDefaultSpecifier = t.importDefaultSpecifier(t.identifier('name'));
// 创建 默认导入节点
const importDeclaration = t.importDeclaration(
[importDefaultSpecifier],
t.stringLiteral('./personal.js')
);
// importDeclaration 就是示例中默认导入语句的节点
importNamespaceSpecifier
含义:
命名空间导入标志,通过该方法可以创建一个命名空间导入标志,配和importDeclaration生成一个命名空间导入节点。Specifier翻译为标志。
JS代码示例:
import * as name from './personal.js' // 命名空间导入语句
babel示例代码:
const t = require('@babel/types');
// 创建 importNamespaceSpecifier 命名空间导入标志
const importNamespaceSpecifierNode = t.importNamespaceSpecifier(t.identifier('name'));
// 创建 importDeclaration 命名空间导入节点
const importDeclarationNode = t.importDeclaration(
[importNamespaceSpecifierNode],
t.stringLiteral('./personal.js')
);
注意:
1、这种导入的场景为:
导出:export const foo = 'foo';
export const bar = 'bar';
那么我们导入的时候可以使用import * as name from './personal.js'导入,name.foo name.bar这种方式调用
导出
exportDefaultDeclaration
含义:
默认导出语句,通过该函数可以实现一个默认导出语句节点
JS代码示例:
function getName(){
return 'xz'
}
export default getName // 这里是一个默认导出语句,这里默认导出getName函数
babel代码示例:
const t = require('@babel/types');
const exportDefaultDeclaration = t.exportDefaultDeclaration(t.identifier('getName'));// 导出什么类型的数据就创建对应的节点即可
// exportDefaultDeclaration就是默认导出语句 --> export default getName;
exportNamedDeclaration
示例一:
含义:
具名导出语句,通过该函数可以实现一个具名导出语句节点。值得注意的是
JS代码示例:
export const myVar = 42; // 具名导出一个myVar变量
babel代码示例:
const t = require('@babel/types');
// 创建变量声明节点
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('myVar'), t.numericLiteral(42)),
]);
// 创建具名导出节点
const exportNode = t.exportNamedDeclaration(declaration, [], null);
示例二:
JS代码示例:
export { // 具名导出多个变量
getName,
setName
}
babel代码示例:
const t = require('@babel/types');
const specifiers = [
t.exportSpecifier(t.identifier('getName'), t.identifier('getName')),
t.exportSpecifier(t.identifier('setName'), t.identifier('setName')),
];
const exportNode = t.exportNamedDeclaration(null, specifiers, null); // specifiers参数主要为导出多个变量准备的,如果导出单个就使用第一个参数
注意:
exportNamedDeclaration方法接受三个参数,分别是 declaration、specifiers 和 source
1、declaration: 这个参数表示要导出的声明,通常是一个变量声明或函数声明的AST节点。在 export const myVar = 42; 这个例子中,declaration 就是表示 const myVar = 42;的 AST 节点。
2、specifiers: 这个参数是一个数组,用于指定额外的导出规范(export specifiers)。在具名导出中,这通常是空数组,因为具名导出已经在 declaration 中指定了要导出的内容
3、source: 这个参数用于指定从哪个源文件导出内容,通常在模块导入和导出时使用。对于具名导出来说,一般情况下可以将其设置为 null 或省略
exportAllDeclaration
含义:
统一导出(或叫全部导出),通过该方法可以实现一个统一导出节点
JS代码示例:
export * from './index.js';
babel代码示例:
const t = require('@babel/types');
// 创建导出全部节点
const exportAllNode = t.exportAllDeclaration(
t.stringLiteral('./index.js'),
);
注意:
在最新的babel/types中这个函数只有一个参数,就是路径参数
导入导出注意点
1、esm中的导出方式有三种:1:默认导出、2:具名导出、3:统一导出
2、一个模块可以同时存在具名导出、默认导出、统一导出
3、当导入一个模块时,你使用哪种导入模式,它就会为你匹配对应模式的导出的值。使用默认导入就会匹配默认导出的值
4、一个模块只能有一个默认导出
5、模块中可以有多个具名导出,每个具名导出都必须使用明确的标识符进行导出,导入方需使用导出模块中指定的标识符名字进行引用
6、因为在esm中,默认导出和具名导出使用了不同的语法和语义规则,因此两者的导出方式会有很大不同
默认导出方式:
对于默认导出,相对于具名导出有很大的自由度。对于默认导出,导出的是什么东西,接收时那个变量就是什么。
1、变量声明和变量导出分开
// 在模块中先声明函数、类或变量
function myFunction() {
// 函数体
}
// 然后使用 export default 导出函数名、类名或变量名即可
export default myFunction;
const obj = {
name: 'xz'
}
export default obj
2、直接在导出时定义并导出
// 导出一个函数
export default function myFunction()
}
// 导出一个类
export default class Personal {
}
3、直接导出字面量
// 直接导出数字、字符串、布尔等字面量
export default 123;
// 直接导出一个对象、数组、函数等
export default {
name: 'xz'
}
具名导出方式:
因为一个模块中可以有多个具名导出,因此具名导出的方式是收到限制的。它不能直接导出字面量
1、导出变量
// 具名导出在导出变量的时候,必须在导出时定义,不允许先定义然后导出变量,即const a = 1; export a 这种是不允许的
export const age = 18
export const obj = { name: 12 } // 导出obj,obj是一个对象
2、导出函数
// 具名导出函数必须导出时定义,不能先定义再导出对应变量
export function getName() { }
export const getName = function (){ }
// 下面这个例子会报错
function getName(){}
export getName // 错误
3、导出类
// 导出时声明,而非先声明后导出
export class Personal {
}
4、导出多个内容
// 示例一
const var1 = 1
const var2 = 2
const var3 = 3
export {
var1,
var2,
var3
}
// 上面导出方式等价于下面的导出方式
export const var1 = 1
export const var2 = 2
export const var3 = 3
// 示例二
function getName(){}
function setName(){}
export {
getName,
setName
}
// 如果你想要实现像默认导出那种先声明再导出的效果,那么导出的时候请用大括号包裹
variableDeclaration
含义:
变量声明,通过该方法可以实现变量声明节点
JS代码示例:
const myVar = 42
babel代码示例:
const t = require('@babel/types');
// 创建变量声明节点:const myVar = 42;
const variableDeclaration = t.variableDeclaration(
'const', // 变量声明类型,可以是 'var', 'let', 或 'const'
[
t.variableDeclarator(
t.identifier('myVar'), // 变量名
t.numericLiteral(42) // 变量值
)
]
);
注意:
1、一般情况下variableDeclaration和variableDeclaration是配合使用的
2、variableDeclaration的第二个参数是一个声明数组当有多个声明元素时会以逗号隔开声明,例如:
const variableDeclaration = t.variableDeclaration(
'const', // 变量声明类型,可以是 'var', 'let', 或 'const' (必须)
[ (必须)
t.variableDeclarator( // myVar = 42的声明
t.identifier('myVar'), // 变量名
t.numericLiteral(42) // 变量值
),
t.variableDeclarator( // age = 22的声明
t.identifier('age'), // 变量名
t.numericLiteral(22) // 变量值
)
]
);
const myVar = 42, age = 22;
3、我们需要把变量声明的赋值和普通赋值区分开,后者通过assignmentExpression方法来实现的
spreadElement
含义:
拓展运算符,通过该函数创建拓展运算符节点
JS代码示例:
...myArray
babel代码示例:
const t = require('@babel/types');
// 创建展开运算符节点:...myArray
const spreadElement = t.spreadElement(
t.identifier('myArray') // 要展开的数组或对象,一般是数组
);
templateLiteral
含义:
模板字符串节点,通过该函数可以创建模板字符串节点
JS代码示例:
`Hello, ${name}!`
babel代码示例:
const t = require('@babel/types');
// 创建模板字符串节点:`Hello, ${name}!`
const templateLiteral = t.templateLiteral(
[
t.templateElement({ raw: 'Hello, ', cooked: 'Hello, ' }, false), // 文本片段
t.templateElement({ raw: '!', cooked: '!' }, true) // 文本片段
],
[
t.identifier('name') // 表达式嵌入部分
]
);
// 示例二:
const t = require('@babel/types');
// 创建模板字符串节点:`Hello, ${name}!----${age}hhhh${city}`
const templateLiteral = t.templateLiteral(
[
t.templateElement({ raw: 'Hello, ', cooked: 'Hello, ' }, false), // 第一个文本片段
t.templateElement({ raw: '!----', cooked: '!----' }, false), // 中间的文本片段
t.templateElement({ raw: 'hhhh', cooked: 'hhhh' }, false) // 中间的文本片段
],
[
t.identifier('name'), // 第一个表达式嵌入部分
t.identifier('age'), // 第二个表达式嵌入部分
t.identifier('city') // 第三个表达式嵌入部分
]
);
// "Hello, ${name}!----${age}hhhh${city}"
注意:
1、templateLiteral有两个参数:quasis,expressions,都为数组类型且必填
2、quasis 是一个包含模板字符串中文本片段的数组。每个文本片段都由 t.templateElement() 创建,包含了原始文本内容和处理后的文本内容。
3、expressions 是一个包含表达式嵌入部分的数组。每个表达式都是 Babel AST 中的节点,可以是变量、函数调用等
4、该函数的拼接原理就是 quasis[0] + expressions[0] + quasis[1] + expressions[1] + .... + quasis[n] + expressions[n],本质上就是先从quasis数组中拿一个,然后从expressions拿一个,然后再从quasis拿一个,这种交替拼接方式
5、在 templateElement 中
raw 属性:包含原始的、未经处理的字符串值。在原始字符串中,特殊字符(如换行符 \n 或制表符 \t)通常以转义字符的形式出现。raw 属性会保留这些转义字符,因此它反映了字符串的原始形式。
cooked 属性:包含经过解析和转义处理后的字符串值。在 cooked 属性中,转义字符被转换为其对应的实际字符,例如 \n 被转换为换行符,\t 被转换为制表符,等等。
const rawString = String.raw`Hello\nWorld`;
console.log(rawString.raw); // 输出结果: Hello\nWorld
console.log(rawString.cooked); // 输出结果: Hello
// World
identifier
含义:
变量,通过这个函数可以创建一个变量节点
JS代码示例:
const a = 1
babel代码示例
const t = require('@babel/types');
// 创建变量声明 const a = 1;
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('a'), t.numericLiteral(1)) // 通过identifier创建一个变量
]);
注意:
1、在ast节点中,identifier这个节点是非常非常普遍的,它代表的是一个变量节点,这个我们想创建一个变量,就可以用它来实现。它也是绝大多数稍微复杂节点的实现基石,很多涉及到变量的节点,都会有它的身影
JSX语法
jsxElement
含义:
jsx元素节点,通过该函数可以实现一个jsx元素节点
JS代码示例:
<div className="container">Hello, {name}!</div>
babel代码示例:
const t = require('@babel/types');
// 创建一个 JSX 元素节点
const element = t.jsxElement(
t.jsxOpeningElement( // 通过jsxOpeningElement创建一个开标签节点
t.jsxIdentifier('div'), // 开标签的标签名
[ // 开标签的属性,数组类型。如果有多个属性,就多传几个属性节点
t.jsxAttribute( // 通过jsxAttribute创建jsx属性节点
t.jsxIdentifier('className'), // 属性名
t.stringLiteral('container') // 属性值
)
],
false // 开标签是否为自闭合,优先级大于jsxElement方法的第二个参数,如果设置为true则第二个参数无效
),
t.jsxClosingElement(t.jsxIdentifier('div')), // 通过jsxClosingElement创建闭合标签节点
[ // 子节点数组。数组中的每一个元素都是一个jsx类型的节点且都是直属子节点
t.jsxText('Hello, '), // jsx文本节点
t.jsxExpressionContainer( // 通过jsxExpressionContainer创建一个jsx表达式节点
t.identifier('name')
),
t.jsxText('!') // jsx文本节点
]
);
// 将 JSX 元素节点转换为代码并输出
const { code } = require('@babel/generator').default(element);
console.log(code); // 输出结果为 <div className="container">Hello, {name}!</div>
注意:
1、t.jsxElement(openingElement, closingElement, children, selfClosing);常用的是前三个,分别表示开标签、闭标签、子节点
2、jsxElement中的子节点表示的是该元素的直属子节点,不是所有子孙节点
jsxSpreadAttribute
含义:
jsx属性展开节点,通过还函数创建一个属性展开节点
JS代码示例:
<div {...props} /> // 属性展开
const t = require('@babel/types');
// 创建 JSX 元素节点
const element = t.jsxElement(
t.jsxOpeningElement(
t.jsxIdentifier('div'),
[
t.jsxSpreadAttribute(t.identifier('props')) // 创建属性展开属性节点
],
true
),
t.jsxClosingElement(t.jsxIdentifier('div')),
[]
);
AST节点常用操作
上面那么多节点的例子,本质上是节点的创建操作,也是节点常用的操作之一。除了创建节点操作之外,还有常用的操作
遍历
一、traverse:
一段字符串代码可以通过 babel.parse()解析成一个完整的ast节点树,而这些节点树是由上面的节点组成的树形结构(当然不止上面这些),然后通过 babel.traverse() 遍历ast节点树。
babel.traverse(ast, options, scope, state, parentPath)有五个参数,分别是:
parent: 父节点,即要遍历的AST的根节点,一般是ast节点树,也可以是某个子节点
options: 一个对象,包含了描述待处理节点类型及其对应处理逻辑的访问器(visitor)
scope:可选参数,表示当前节点的作用域。这个参数通常可以不传,babel会自动处理作用域。
state:表示额外的状态信息,可以在 visitor 中共享和修改,该参数在遍历到任何节点时都可以访问并修改,相当于遍历时的一个信息暂存区(redux)
parentPath:可选参数,表示父路径,即当前节点在 AST 中的路径信息。
代码示例:
const babel = require('@babel/core');
const code = `
function square(n) {
return n * n;
}
`;
const ast = babel.parse(code, { sourceType: "module", plugins: ["jsx"]})
babel.traverse(ast, {
enter(path) {
console.log(path.node.type);
}
})
二、visitor这里我们需要了解一下第二个参数options,即包含visitor的对象,该对象中定义的通常时遍历节点用到的visitor函数
const options = {
enter(path) {
// 进入节点时执行的操作
},
exit(path) {
// 离开节点时执行的操作
},
FunctionDeclaration(path) {
// 当遍历到函数声明节点时执行该函数
},
VariableDeclaration(path) {
// 当遍历到变量声明节点时执行该函数
}
// 其他节点类型的处理函数...
}
注意:
1、所有的visitor函数都会接收一个path参数,该参数是当前遍历的节点的path路径。
2、visitor函数名必须和节点名称保持一致,否者无法正确识别。通用函数(enter、exit)除外。
3、当遍历到对应节点时,babel会将该节点的path对象传入对应visitor函数中供用户操作
4、visitor函数的类型本质上没有限制,只要是符合babel中的节点类型,都可以作为visitor函数来调用(官方解释)
5、path 包含了当前节点及其父节点等信息
三、path 对象通常包含以下常用属性和方法:
node: 当前节点的 AST 节点对象。
parent: 当前节点的父节点。
scope: 当前节点的作用域。
get(key): 获取节点的指定属性值。
traverse(visitor): 在当前节点上继续遍历,并应用指定的访问器对象。
replaceWith(node): 用新的节点替换当前节点。
remove(): 移除当前节点。
四、enter函数和exit函数在Babel的AST遍历过程中有以下区别:
(1) enter 函数执行:
1、当遍历器到达某个节点时,首先会执行该节点对应的 enter 函数。
2、在 enter函数中,您可以对当前节点及其子节点进行处理。
(2) 子节点遍历:
1、在执行完当前节点的 enter 函数后,遍历器会继续向下遍历子节点,并依次执行子节点的 enter 和 exit 函数。
(3) exit 函数执行:
1、当遍历器从子节点返回到父节点时,会执行子节点对应的 exit 函数。
2、在 exit 函数中,通常用于处理完整个节点及其子节点后的清理工作或额外的逻辑。
修改
代码模板:
const babel = require('@babel/core');
const t = require('@babel/types');
const code = `
function add(a, b) {
return a + b;
}
const a = [1,2,3]
`;
const visitor = {
// visitor函数
};
const { code: modifiedCode } = babel.transformSync(code, {
plugins: [{ visitor }],
});
console.log(modifiedCode);
一、path.remove() 删除当前节点(包含所有子节点)
const visitor = {
FunctionDeclaration(path) {
// 判断当前节点是否是 add 函数
if (path.node.id.name === 'add') {
path.remove(); // 删除当前节点
}
},
};
//剩余代码如下, 删除了add函数的声明
const a = [1, 2, 3];
二、path.replaceWith(node) 用一个新节点替换当前节点
const visitor = {
FunctionDeclaration(path) {
// 判断当前节点是否是 add 函数
if (path.node.id.name === 'add') {
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
const greetMethod = t.objectMethod(
'method',
t.identifier('greet'),
[],
t.blockStatement([])
);
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
]);
path.replaceWith(declaration); // 用最新节点代替原节点
}
},
};
// 修改后节点后生成代码如下,我们新建一个对象定义节点来代替当前的函数声明节点
const person = {
name: "Alice",
age: 30,
greet() {}
};
const a = [1, 2, 3];
三、path.insertBefore(nodes) 在当前节点之前插入一个或多个节点。想插入多个节点就传入一个数组[node1, node2]
const visitor = {
FunctionDeclaration(path) {
// 判断当前节点是否是 add 函数
if (path.node.id.name === 'add') {
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
const greetMethod = t.objectMethod(
'method',
t.identifier('greet'),
[],
t.blockStatement([])
);
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
]);
path.insertBefore(declaration);
}
},
};
// 修改节点后生成的代码如下,我们声明了一个对象节点,在add函数声明前插入
const person = {
name: "Alice",
age: 30,
greet() {}
};
function add(a, b) {
return a + b;
}
const a = [1, 2, 3];
四、path.insertAfter(nodes) 在当前节点之后插入一个或多个节点。想插入多个节点就传入一个数组[node1, node2]
const visitor = {
FunctionDeclaration(path) {
// 判断当前节点是否是 add 函数
if (path.node.id.name === 'add') {
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
const greetMethod = t.objectMethod(
'method',
t.identifier('greet'),
[],
t.blockStatement([])
);
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
]);
path.insertAfter(declaration); // 在当前节点后插入一个对象定义节点
}
},
};
// 修改节点后生成的代码如下,我们声明了一个对象节点,在add函数声明后插入
function add(a, b) {
return a + b;
}
const person = {
name: "Alice",
age: 30,
greet() {}
};
const a = [1, 2, 3];
五、path.replaceWithMultiple(nodes) 用多个节点替换当前节点
const visitor = {
FunctionDeclaration(path) {
// 判断当前节点是否是 add 函数
if (path.node.id.name === 'add') {
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
const greetMethod = t.objectMethod(
'method',
t.identifier('greet'),
[],
t.blockStatement([])
);
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
]);
const declaration2 = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person2'), objectExpression)
]);
path.replaceWithMultiple([declaration, declaration2]); // 用两个对象定义来替代当前节点
}
},
};
// 修改节点后生成的代码如下,用两个对象定义来替代add函数声明
const person = {
name: "Alice",
age: 30,
greet() {}
};
const person2 = {
name: "Alice",
age: 30,
greet() {}
};
const a = [1, 2, 3];
生成
生成部分主要是将ast节点重新生成为字符串代码,通常使用generator函数实现,因为这里举例的是直接将ast节点转为代码字符串,因此没有使用到babel/core中的函数,babel/core中集成了parse和transform,前者是将代码字符串转为ast,后者是一套完成流程,即将code转为ast,然后通过我们传入的visitor操作ast节点然后将其再转为code。将这个流程封装为一个函数了,因此对用户来说不可见。
如果我们想要自由度更高的ast操作,可以尝试分别使用@babel/parser、@babel/traverse、@babel/generator这三个包。如果业务中就是code -> code全流程,那么可以考虑使用@babel/core这个包
const t = require('@babel/types');
const generator = require('@babel/generator').default
const nameProperty = t.objectProperty(t.identifier('name'), t.stringLiteral('Alice'));// objectProperty创建属性节点
const ageProperty = t.objectProperty(t.identifier('age'), t.numericLiteral(30));
const greetMethod = t.objectMethod(
'method',
t.identifier('greet'),
[],
t.blockStatement([])
);
const objectExpression = t.objectExpression([nameProperty, ageProperty, greetMethod]); // 数组:表示内部属性
const declaration = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('person'), objectExpression)
])
const { code } = generator(declaration)
console.log(code);
// 下面是生成的code代码
const person = {
name: "Alice",
age: 30,
greet() {}
};
注意:
1、我们这里是cjs格式来引入@babel/generator包,如果是使用esm格式引入可能导入的方式还不一样。官网只提供了esm这种格式