AST
为什么要谈AST(抽象语法树)?
如果你查看目前任何主流的项目中的devDependencies,会发现前些年的不计其数的插件诞生。我们归纳一下有:javascript转译、代码压缩、css预处理器、elint、pretiier,Tree-Sharking等。有很多js模块我们不会在生产环境用到,但是它们在我们的开发过程中充当着重要的角色。
所有的上述工具,不管怎样,都建立在了AST这个巨人的肩膀上
- 代码风格,语法的检查,IDE中的错误提示,格式化,自动补全等等
- 优化变更代码,代码压缩,Tree-Sharking等等
- es6转es5,以及TypeScript、JSX等转化为原生Javascript等等
整体思路
写个简单的有引入的文件,然后npx webpack打包一下看下打包出来的内容是什么。
1 | (function (modules) { |
AST解析语法树
这一步就是parse的内容,解析读取到的模块的内容。
- 替换require,替换依赖的路径,把修改后的模板放进sourceCode
- 把依赖放进dependencies数组中
用到了几个库来做这件事:
- babylon 主要是把源码解析成AST
- @babel/traverse 遍历节点(遍历到对应的节点)以深度优先的形式遍历AST,并进行修改和转化
- @babel/types 替换遍历到的节点
- @babel/generator 替换好的结果生成,根据AST生成新的代码
- (traverse和generator是es6模块 引用的时候要require(‘@babel/traverse’).default 不然默认导出的是一个对象)
- @babel/core Babel核心库,被很多babel配套设施依赖,用于加载 preset 和 plugin
Babel转化的核心链路:原始代码-原始AST -转化后的AST-转化后的代码
在线生成AST
可以在这个网站直观的查看AST节点: https://astexplorer.net/
实现
由于ES6转ES5中需要用到babel,所以要用到一下插件1
cnpm i @babel/core @babel/parser @babel/traverse @babel/preset-env -D
1 | { |
需要的文件
使用webpack肯定少不了原文件,我们会涉及三个需要打包的js文件(entry.js
、message.js
、name.js
)1
2
3
4// entry.js
import message from './message.js';
console.log(message);
1 | // message.js |
1 | // name.js |
1 | //bundler.js |
entry.js
就是我们的入口文件,文件的依赖关系是,entry.js
依赖message.js
,message.js
依赖name.js
。
bundler.js
是我们简易版的webpack
目录结构1
2
3
4
5- example
- entry.js
- message.js
- name.js
- bundler.js
如何分析依赖
webpack分析依赖是从一个入口文件开始分析的,当我们把一个入口的文件路径传入,webpack就会通过这个文件的路径读取文件的信息(读取到的本质其实是字符串),然后把读取到的信息转成AST(抽象语法树),简单点来说呢,就是把一个js文件里面的内容存到某种数据结构里,里面包括了各种信息,其中就有当前模块依赖了哪些模块。我们暂时把通过传文件路径能返回文件信息的这个函数叫 createAsset
。
createAsset
返回什么
第一步我们肯定需要先从 entry.js
开始分析,于是就有了如下的代码,我们先不关心createAsset
具体代码是怎么实现的,具体代码我会放在最后。1
createAsset("./example/entry.js");
当执行这句代码,createAsset
会返回下面的数据结构,这里包括了模块的id,文件路径,依赖数组(entry.js
依赖了message.js
,所以会返回依赖的文件名),code(这个就是entry.js
ES6转ES5的代码)
通过 createAsset
我们成功拿到了entry.js
的依赖,就是 dependencies
数组。
createGraph
返回什么,如何找下一个依赖
我们通过上面可以拿到entry.js依赖的模块,于是我们就可以接着去遍历dependencies
数组,循环调用createAsset
这样就可以得到全部模块相互依赖的信息。想得到全部依赖信息需要调用 createGraph
这个一个函数,它会进行广度遍历,最终返回下面的数据
我们可以看到返回的数据,字段之前都和大家解释了,除了 mapping
,mapping
这个字段是把当前模块依赖的文件名称 和 模块的id 做一个映射,目的是为了更方便查找模块。
bundle
返回什么 && 最后步骤
我们现在已经能拿到每个模块之前的依赖关系,我们再通过调用bundle
函数,我们就能构造出最后的bundle.js
,输出如下图
源码
1 | const fs = require("fs"); |
打包生成: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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
(function (modules) {
// 创建一个require()函数: 它接受一个 模块ID 并在我们之前构建的模块对象查找它.
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
// 根据mapping的路径,找到对应的模块id
return require(mapping[relativePath]);
}
const module = { exports: {} };
// 执行每个模块的代码。
fn(localRequire, module, module.exports);
return module.exports;
}
// 执行入口文件,
require(0);
})({
0: [
function (require, module, exports) {
;
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_message.default);
},
{ "./message.js": 1 },
], 1: [
function (require, module, exports) {
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _name = require("./name.js");
var _default = "hello ".concat(_name.name, "!");
exports.default = _default;
},
{ "./name.js": 2 },
], 2: [
function (require, module, exports) {
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.name = void 0;
var name = 'world';
exports.name = name;
},
{},
],
})