问题背景
如何获取js的依赖关系
AST - 抽象语法树
是源代码语法结构的一种抽象表示。它以 树状 的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,会省略掉一些无具体意义的分隔符如 ;、{} 等等。
列如以下代码:
function square(n) {
return n * n;
}
生成:
通过fs读取文件
- 通过正则匹配内容“./foo.js”
- 通过ast获取内容“./foo.js”
用途
AST在前端无处不在,我们熟悉的开发工具几乎全依赖于AST进行开发:Webpack、Babel、Eslint、Prettier。如果你想了解 JavaScript 编译执行的原理并掌握其精髓,那么你就必须得了解 AST。
ast的生成过程
1. 词法分析
主要是将字符流(char stream)转换为令牌流(token stream),简单来说就是在源代码的基础上进行分词
let a = 10;
===>
let` 、`a`、`=`、`10`、 `;
词法分析器 从左往右逐个字符扫描分析 整个程序的字符串,当遇到不同的字符时,会驱使它迁移到不同的状态。在扫描字符的时候,遇到 c 字母,如果后面还有字符,将继续扫描,直到遇到空格,识别出 const,发现是一个关键字,将其生成词法单元 { type: 'Keyword', value: 'const' },然后接着扫描,以此类推,生成 Token List。
列如:
[
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '10' },
{ type: 'Punctuator', value: ';' },
];
通过上面分解出来的词法分析结果,我们可以观察到,缺少一些比较关键的信息:
- 没有任何语法信息;
- 体现不了代码的执行顺序;
2. 语法分析
符号表填项:扫描源程序,遇到一个名字就检索表,若它是一个新的名字,就填表,在调用词法语法分析程序时,还要把有关的信息填入
语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST) 。同时,验证语法,语法如果有错的话,将抛出语法错误。
词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会在读取所有的词法记号后再使用语法分析器来处理。通常情况下,每取得一个词法记号,就将其送入语法分析器进行分析。
节点
{ type: "FunctionDeclaration", id: {...}, params: [...], body: {...} }
ast的每一层结构,被称为 节点(Node) ,每一个节点都包含 type 属性,用于表示节点类型,比如:FunctionDeclaration、Identifier、BinaryExpression 等等。除此之外,Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置,比如: start、end、loc。
节点类型
主要分为以下几个大类:字面量、标志符、语句、声明、表达式、注释 等等
1. 字面量 Literal
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
StringLiteral | 字符型字面量 | 通常指字符串类型的字面量:'Hello, AST!' |
NumericLiteral | 数值型字面量 | 通常指数字类型的字面量:123 |
BooleanLiteral | 布尔型字面量 | 通常指布尔类型值:true / false |
RegExpLiteral | 正则型字面量 | 通常指正则表达式:/[0-9]/ |
TemplatLiteral | 模板型字面量 | 通常指模板字符串(`) |
2. 标志符Identifier
程序中所有的 变量名、函数名、对象键(key) 以及函数中的参数名,都属于标志符(Identifier)。
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. 声明 Declaration
声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个 变量、函数、class、import、export 等。
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
VariableDeclaration | 变量声明 | const a = 10; |
FunctionDeclaration | 函数声明 | function sum() {} |
ImportDeclaration | 模块引入声明 | import { reactive } from 'vue;' |
ExportDefaultDeclaration | 模块默认导出声明 | export default a = 10; |
5. 表达式 Expression
表达式的特点是执行完以后有返回值,这是和语句 (statement) 的区别
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
ArrayExpression | 数组表达式 | 通常指一个数组:[1, 2, 3] |
ArrowFunctionExpression | 箭头函数表达式 | () => {} |
AssignmentExpression | 赋值表达式 | 通常指为一个变量赋值,比如 a = 1 |
BinaryExpression | 二元表达式 | 1 + 2 |
UnaryExpression | 一元表达式 | -1 |
FunctionExpression | 函数表达式 | function(){} |
6. Comment & Program
| 类型名称 | 中文译名 | 描述 |
|---|---|---|
Program | 程序主体 | 整段代码的主体 |
CommentBlock | 块级注释 | /* 块级注释 */ |
CommentLine | 单行注释 | // 单行注释 |
babel 中的使用
@babel/parser
// → 导入模块 const parser = require('@babel/parser');
// → 定义一段代码字符串
const codeString = `function square(n) { return n * n; }`;
// → 解析代码字符串
const ast = parser.parse(codeString, {
sourceType: 'script', // module unambigious
plugins: ['jsx', 'typescript'],
});
@babel/traverse
const traverse = require('@babel/traverse');
// → 遍历节点
traverse.default(ast, {
Identifier(path) { // 判断是否是 name 为 n 的标志符
if (path.node.name === 'codeString') {
path.node.name = 'codeObj';
}
}
});
Path(路径)
AST 通常会有许多节点,我们可以通过 path 对象表示节点之间的关联关系,通过这个对象提供的属性方法,我们可以 操作 AST 语法树。
属性:
-
path.node:当前遍历到的 node 节点,可通过它访问节点上的属性,对于 Ast 节点; -
path.parent:父级 node,无法进行替换; -
path.parentPath:父级 path,可进行替换; -
path.scope:作用域相关,可用于变量重命名,变量绑定关系检测等; -
path.key:获取路径所在容器的索引 -
path.listKey:获取容器的key -
path.container:获取路径的容器(包含所有同级节点的数组) -
path.inList:判断路径是否有同级节点
方法:
-
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(): 不往下遍历,跳过该节点
@babel/generate
const generator = require('@babel/generator');
generator.default(ast).code;
@babel/core
// → 同步方法
transformSync(code, options); // => { code, map, ast }
transformFileSync(filename, options); // => { code, map, ast }
transformFromAstSync(parsedAst, sourceCode, options); // => { code, map, ast }
// → 异步方法
transformAsync('code();', options).then((result) => {});
transformFileAsync('filename.js', options).then((result) => {}); transformFromAstAsync(parsedAst, sourceCode, options).then((result) => {});