学习webpack原理

打包

首先来看看webpack打包生成了什么。平常我们写代码,build之后会发现产生了一坨混合了webpack 相关代码的文件,其实这些wepack函数实现了模块的引入。比如下面的index.js

1
2
import { plus } from './plus';
console.log(plus(1+2, 3))

plus.js:

1
export const plus = (a, b) => a + b

通过webpack build 之后,主要生成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(modules) { // webpackBootstrap
var installedModules = {} // 缓存
function __webpack_require__(moduleId) {}
...
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({

"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
...}),
"./src/plus.js": (function(module, __webpack_exports__, __webpack_require__) {
...})
});

这么一坨看上去复杂,其实就是一个即时函数而已:

1
2
3
4
5
6
7
8
9
10
function webpackBootstrap(modules){...}

var modules = {
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
...}),
"./src/plus.js": (function(module, __webpack_exports__, __webpack_require__) {
...})
}

webpackBootstrap(modules)

modules是以模块路径为key,以一个函数为value的对象。当这个模块被引用时,相当于就是调用了 __webpack_require__(key),那么__webpack_require__是什么呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function  __webpack_require__(moduleId){
// 基于commonJS规范,webpack定义一个module对象,里面的 export 就是 module.exports
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// modules[moduleId] 就是上面所说的对象中value(函数)
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 标记模块已经被导入
module.l = true;
}

回到最开始,index.js中的import会被转为:

1
2
3
4
5
6
7
import { plus } from './plus';
===>
{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
var _plus_xxx_MODULE_0__ = __webpack_require__(\"./src/plus.js\");
})
}

而在./src/plus.js中,webpackmodule.export对象上加了个属性:

1
2
3
4
5
6
7
{
"./src/plus.js": (function(module, __webpack_exports__, __webpack_require__) {
// __webpack_exports__ 就是传入的module.exports
__webpack_require__.d(__webpack_exports__, \"plus\", function() { return plus; });
const plus = (a, b) => a + b;
})
}

__webpack_require__.dObject.defineProperty的意义:

1
2
3
4
5
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};

建立引用关系

大概看懂了打包生成文件的结构,那么接下来看看webpack是怎么生成这些东西的呢?让我们跟随下面这一个webpack简易版的代码来进行理解:

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
// 在这个例子中,我们将创建一个 依赖关系图 并将其用于打包
// 它的所有模块都捆绑在一起.

// >请注意: 这是一个非常简化的例子
// 对这些例子仅仅执行一次循环依赖,缓存模块导出和解析每个模块
// 其他方面的处理都跳过,使这个例子尽可能简单.

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('./entry.js');
const result = bundle(graph);

其中createAsset函数通过Ast来分析import,然后添加到该moduledependencies;通过entry入口,来循环入队,分析子module依赖,建立引用关系。

上面的代码里只是最简单的JS文件打包,那么别的文件类型如何处理呢?这就需要Loader了,对JS文件进行createAsset,而对其他文件先用相应Loader进行处理成js模块,再进行引入。

引用:

mini webpack

Vue Loader

PS:

同一种文件也能用不同的loader,比如file-loaderurl-loader

  1. file-loader 可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。此外,这意味着 你可以就近管理图片文件,可以使用相对路径而不用担心部署时 URL 的问题。使用正确的配置,webpack将会在打包输出中自动重写文件路径为正确的 URL。
  2. url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。