-
Notifications
You must be signed in to change notification settings - Fork 64
/
minipack.js
216 lines (157 loc) · 9.13 KB
/
minipack.js
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//
// 模块捆绑器 将 小块代码 编译成 更大和更复杂的代码,可以运行在Web浏览器中.
// 这些小块只是JavaScript文件以及它们之间的依赖关系,而这正是由模块系统表示
// https://webpack.js.org/concepts/modules
// 模块捆绑器具有 入口文件 的这种概念,而不是添加一些
// 脚本标签在浏览器中并让它们运行,我们让 捆绑器 知道
// 哪个文件 是我们应用程序的 主要文件. 该文件能引导
// 我们的整个应用程序.
// 我们的打包程序将从该 入口文件 开始,并尝试理解
// 它依赖于哪些文件. 然后,它会尝试了解哪些文件
// 依赖关系取决于它,它会继续这样做,直到它发现
// 我们应用程序中的 每个模块,以及它们如何 相互依赖.
// 这种对项目的理解被称为`依赖图`.
// 在这个例子中,我们将创建一个 依赖关系图 并将其用于打包
// 它的所有模块都捆绑在一起.
// 让我们开始 : )
// >请注意: 这是一个非常简化的例子
// 对这些例子仅仅执行一次循环依赖,缓存模块导出和解析每个模块
// 其他方面的处理都跳过,使这个例子尽可能简单.
const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');
let ID = 0;
// 我们首先创建一个函数,该函数将接受 文件路径 ,读取内容并提取它的依赖关系.
function createAsset(filename) {
// 以字符串形式读取文件的内容.
const content = fs.readFileSync(filename, 'utf-8');
// 现在我们试图找出这个文件依赖于哪个文件. 我们可以通过查看其内容
// 来获取 `import` 字符串. 然而,这是一个非常笨重的方法,所以我们将使用JavaScript解析器.
// JavaScript解析器是可以读取和理解JavaScript代码的工具.
// 它们生成一个更抽象的模型,称为`ast (抽象语法树)`.
// 我强烈建议你看看[`ast explorer`](https://astexplorer.net) 看看 `ast` 是如何的
// `ast`包含很多关于我们代码的信息. 我们可以查询它了解我们的代码正在尝试做什么.
const ast = babylon.parse(content, {
sourceType: 'module',
});
// 这个数组将保存这个模块依赖的模块的相对路径.
const dependencies = [];
// 我们遍历`ast`来试着理解这个模块依赖哪些模块.
// 要做到这一点,我们检查`ast`中的每个 `import` 声明. ❤️
traverse(ast, {
// `Ecmascript`模块相当简单,因为它们是静态的. 这意味着你不能`import`一个变量,
// 或者有条件地`import`另一个模块.
// 每次我们看到`import`声明时,我们都可以将其数值视为`依赖性`.
ImportDeclaration: ({node}) => {
// 我们将依赖关系数组推入我们导入的值. ⬅️
dependencies.push(node.source.value);
},
});
// 我们还通过递增简单计数器为此模块分配唯一标识符.
const id = ID++;
// 我们使用`Ecmascript`模块和其他JavaScript功能,可能不支持所有浏览器.
// 为了确保`我们的bundle`在所有浏览器中运行,
// 我们将使用[babel](https://babeljs.io)来传输它
// 该`presets`选项是一组规则,告诉`babel`如何传输我们的代码.
// 我们用`babel-preset-env``将我们的代码转换为浏览器可以运行的东西.
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
// 返回有关此模块的所有信息.
return {
id,
filename,
dependencies,
code,
};
}
// 现在我们可以提取单个模块的依赖关系,我们将通过提取`入口文件{entry}`的依赖关系来解决问题.
// 那么,我们将提取它的每一个依赖关系的依赖关系. 循环下去
// 直到我们了解应用程序中的每个模块以及它们如何相互依赖. 这个项目的理解被称为`依赖图`.
function createGraph(entry) {
// 首先解析整个文件.
const mainAsset = createAsset(entry);
// 我们将使用`队列{queue}`来解析每个`资产{asset}`的依赖关系.
// 我们正在定义一个只有 入口资产{entry asset} 的数组.
const queue = [mainAsset];
// 我们使用一个`for ... of`循环遍历 队列.
// 最初 这个队列 只有一个 资产,但是当我们迭代它时,我们会将额外的 新资产 推入 队列 中.
// 这个循环将在 队列 为空时终止.
for (const asset of queue) {
// 我们的每一个 资产 都有它所依赖模块的相对路径列表.
// 我们将重复它们,用我们的`createAsset() `函数解析它们,并跟踪此模块在此对象中的依赖关系.
asset.mapping = {};
// 这是这个模块所在的目录.
const dirname = path.dirname(asset.filename);
// 我们遍历其相关路径的列表
asset.dependencies.forEach(relativePath => {
// 我们的`createAsset()`函数需要一个绝对文件名.
// 但是该依赖关系数组是保存了相对路径的数组.
// 这些路径是相对于导入他们的文件.
// 我们可以通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径.
const absolutePath = path.join(dirname, relativePath);
// 解析资产,读取其内容并提取其依赖关系.
const child = createAsset(absolutePath);
// 了解`asset`依赖取决于`child`这一点对我们来说很重要.
// 通过给`asset.mapping`对象增加一个新的属性(值为child.id)来表达这种一一对应的关系.
asset.mapping[relativePath] = child.id;
// 最后,我们将`child`这个资产推入队列,这样它的依赖关系也将被迭代和解析.
queue.push(child);
});
}
// 到这一步,队列 就是一个包含目标应用中 每个模块 的数组:
// 这就是我们的表示图.
return queue;
}
// 接下来,我们定义一个函数,它将使用我们的`graph`并返回一个可以在浏览器中运行的包.
// 我们的包将只有一个自我调用函数:
// `(function() {})()`
// 该函数将只接收一个参数: 一个包含`graph`中每个模块信息的对象.
function bundle(graph) {
let modules = '';
// 在我们到达该函数的主体之前,我们将构建一个作为该函数的参数的对象.
// 请注意,我们构建的这个字符串被两个花括号 ({}) 包裹,因此对于每个模块,
// 我们添加一个这种格式的字符串: `key: value,`.
graph.forEach(mod => {
// 图表中的每个模块在这个对象中都有一个`entry`. 我们使用`模块的id`作为`key`和一个数组作为`value` (用数组因为我们在每个模块中有2个值) .
// 第一个值是用函数包装的每个模块的代码. 这是因为模块应该被 限定范围: 在一个模块中定义变量不会影响 其他模块 或 全局范围.
// 我们的模块在我们将它们`转换{被 babel 转译}`后, 使用`commonjs`模块系统: 他们期望一个`require`, 一个`module`和`exports`对象可用. 那些在浏览器中通常不可用,所以我们将它们实现并将它们注入到函数包装中.
// 对于第二个值,我们用`stringify`解析模块及其依赖之间的关系(也就是上文的asset.mapping). 解析后的对象看起来像这样: `{'./relative/path': 1}`.
// 这是因为我们模块的被转换后会通过相对路径来调用`require()`. 当调用这个函数时,我们应该能够知道依赖图中的哪个模块对应于该模块的相对路径.
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});
// 最后,我们实现自调函数的主体.
// 我们首先创建一个`require()`⏰函数: 它接受一个 `模块ID` 并在我们之前构建的`模块`对象查找它.
// 通过解构`const [fn, mapping] = modules[id]`来获得我们的包装函数 和`mappings`对象.
// 我们模块的代码通过相对路径而不是模块ID调用`require()`.
// 但我们的`require`🌟函数接收 `模块ID`. 另外,两个模块可能`require()`相同的相对路径,但意味着两个不同的模块.
// 要处理这个问题,当需要一个模块时,我们创建一个新的,专用的`require`函数供它使用.
// 它将是特定的,并将知道通过使用`模块的mapping对象`将 `其相对路径` 转换为`ID`.
// 该mapping对象恰好是该特定模块的`相对路径和模块ID`之间的映射.
// 最后,使用`commonjs`,当模块需要被导出时,它可以通过改变exports对象来暴露模块的值.
// require函数最后会返回exports对象.
const result = `
(function(modules) {
function require(id) { //🌟
const [fn, mapping] = modules[id];
function localRequire(name) { //⏰
return require(mapping[name]); //🌟
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
// 我们只需返回结果,欢呼!:)
return result;
}
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
console.log(result);