webpack源码分析一:AST语法
# 0.前言
本篇博客为webpack_deom01 (opens new window)项目仓库笔记,主要有以下内容:
AST
的含义及其应用Babel
中操作AST
树的API
说明。- 使用
AST
操作完成两个案例:- 将
let
转化为var
。 - 将
import
导入的模块保存在数组中(depRelation
),实现对模块的静态分析。
- 将
# 1.什么是AST,及AST的应用
AST
是 Abstract Syntax Tree
的简称,是前端工程化绕不过的一个名词。它涉及到工程化诸多环节的应用,比如:
- 如何将
Typescript
转化为Javascript
(typescript
) - 如何将
SASS/LESS
转化为CSS
(sass/less
) - ⭐️如何将
ES6+
转化为ES5 (babel)
- 如何将
Javascript
代码进行格式化 (eslint/prettier
) - 如何识别 React 项目中的
JSX
(babel
) - ❓
GraphQL
、MDX
、Vue SFC
等等
而在语言转换的过程中,实质上就是对其 AST
的操作,核心步骤就是 AST
三步走
Code -> AST (Parse)
AST -> AST (Transform)
AST -> Code (Generate)

在案例1
中使用babel
完成AST
转换,实现变量声明(VariableDeclaration
)语法的降级(将let
改为var
)。
初始code
为:
const code = `let a = 'let'; let b = 2`;
let a = 'let'
将被解析为以下的AST
树结构:
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "StringLiteral",
"extra": {
"rawValue": "let",
"raw": "'let'"
},
"value": "let"
}
}
],
"kind": "let"
},
...
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上述 AST
基本将 code
的行为做一个很好的表征:
等式左侧:
- 变量声明:
type: "VariableDeclaration"
- 类型:
kind: "let"
- 变量声明:
等式右侧:
- 值的类型:字符串
StringLiteral
、数值NumericLiteral
- 值为:
value: "let"
- 值的类型:字符串
# 2.AST树操作
这部分主要借助的Babel
库提供的三个底层的API
:
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
2
3
转化操作:
const code = `let a = 'let'; let b = 2`;
// parse: Code => AST(引用地址)
const ast = parse(code, { sourceType: "module" });
// traverse : AST => Code
// 无输出,直接修改的原 AST 树
traverse(ast, {
enter: (item) => {
if (item.node.type === "VariableDeclaration") {
if (item.node.kind === "let") {
item.node.kind = "var";
}
}
},
});
// generate
const result = generate(ast, {}, code);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3. Babel 家族简介
一般情况而言,我们不会直接使用parser
、traverse
、generator
这三个 Api
对 AST
树进行操作,Babel
已经封装好更为简洁的操作AST
树的API
,这些库封装在@babel/core
,主要提供以下两类AST
操作能力:
可以直接生成
code
的API
:从
code
转化为code
transform
、transformSync
从
file
转化为code
transformFile
、transformFileSync
:大部分情况下,code
的形式不是以字符串的形式,而是需要通过fs.readFile
的形式读取的,具体见案例3
。
从
Ast
转化为code
transformFromAst
、transformFromAstSync
:
与解析相关的
API
:parse
:仅支持babel v7
之前的版本,v8
不再对该api
向后兼容。- 新版请使用:
parseSync
和parseAsync
详细,各
APi
的使用见:@babel/core (opens new window) 官方文档
上述api
在使用时,还可在转化时设置option
,如在babel
中内置着许多的preset
(预制)
const result = babel.transformFromAstSync(ast, es6Code, {
presets: ["@babel/preset-env"],
});
2
3
其中, @babel/preset-env
是一个智能预设,允许当前环境使用最新的 JavaScript
而不需要针对目标宿主环境进行额外的调整,preset-env
将会所有 ES2015-ES2020
代码自动转化为 ES5
代码。
具体实践见:案例3
# 4.使用AST树:分析 Webpack 模块依赖
在【案例4】中,存在 4
个 project
文件夹,分别对应着以下几种情况:
project_1
与deps.ts
文件:基础模块分析实现。project_2
与deps2.ts
文件:递归分析深层依赖。project_3
与deps3.ts
文件:可以对循环依赖进行静态分析。
核心实现原理:
检查代码中是否有 import "./xxx.js"
这段代码,也即 AST
树中搜索 path.node.type
是否有ImportDeclaration
类型。如果存在则将path.node.source.value
中的内容存入depRelation
数组中。
traverse(ast, {
enter: (path) => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(
dirname(filepath),
path.node.source.value
);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
depRelation[key].deps.push(depProjectPath);
collectCodeAndDeps(depAbsolutePath);
}
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.总结:
AST相关:
parse
: 把代码code
变成AST
。traverse
: 遍历AST
进行修改。generate
: 把AST
变成代码code2
。
工具:
@babel/parser
@babel/traverse
@babel/generator
@babel/core
包含前三者@babel/preset-env
内置了ES6
转ES5
的很多规则
构建了一个
dep.ts
函数实现对模块的依赖分析。