webapck 代码分离原理
通过上文,我们了解到所有依赖文件源码最终都会以映射形式存在对应bundle中。
如果所有文件最终打包成一个文件,没有什么问题,但是如果是打包成多个文件那就可能造成大量冗余代码。因为如果多个入口中有相同的依赖,那么依赖源码会被打到每个bundle中造成冗余。
为了防止重复代码,webpack提供了多种解决方法,这里我们以dependOn来介绍下代码分离的原理。
配置 [dependOn](https://webpack.docschina.org/configuration/entry-context/#dependencies)
option 选项,这样可以在多个 chunk 之间共享模块:
const config: webpack.Configuration = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared'
},
other: {
import: './src/other.js',
dependOn: 'shared'
},
shared: 'conversion_radix'
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
打包后的index.bundle.js:
(self["webpackChunkbasic"] = self["webpackChunkbasic"] || []).push([["index"],{
"./src/index.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 源码省略
...
})
})()
2
3
4
5
6
上文我们知道打包不进行代码分割时,最终bundle里会维护一个_webpack_modules_
对象记录所有引用文件的源码,并且实现一套自己的require方法。
经过代码分割后入口bundle文件里并没有定义require方法,也没有依赖映射对象,而仅仅是包含自身依赖的非共享模块源码,并且被push到一个全局数组中。
我们再看打包之后会额外生成一个shared.bundle.js:
(() => {
var __webpack_modules__ = ({
"./node_modules/conversion_radix/lib/index.js": (function(__unused_webpack_module, exports) {
... //依赖源码
})
});
// 缓存模块
var __webpack_module_cache__ = {};
// 定义require
function __webpack_require__(moduleId) {...}
// 将共享模块依赖添加到__webpack_require__的静态属性上
__webpack_require__.m = __webpack_modules__;
(() => {
var deferred = [];
__webpack_require__.O = (result, chunkIds, fn, priority) => {
if(chunkIds) {
priority = priority || 0;
for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) {
deferred[i] = deferred[i - 1];
}
deferred[i] = [chunkIds, fn, priority];
return;
}
var notFulfilled = Infinity;
for (var i = 0; i < deferred.length; i++) {
var [chunkIds, fn, priority] = deferred[i];
var fulfilled = true;
for (var j = 0; j < chunkIds.length; j++) {
if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
chunkIds.splice(j--, 1);
} else {
fulfilled = false;
if(priority < notFulfilled) notFulfilled = priority;
}
}
if(fulfilled) {
deferred.splice(i--, 1);
var r = fn();
if (r !== undefined) result = r;
}
}
return result;
};
})();
(() => {
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = (module) => {
var getter = module && module.__esModule ?
() => (module['default']) :
() => (module);
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();
// 定义__webpack_require__的n,r,o属性,见上文解析,此处省略
...
(() => {
// no baseURI
// 记录加载中或已加载的chunks
// undefined = chunk未加载, null = chunk预加载
// [resolve, reject, Promise] = chunk 加载中, 0 = chunk已加载
var installedChunks = {
"shared": 0
};
// no chunk on demand loading
// no prefetching
// no preloaded
// no HMR
// no HMR manifest
__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
// 加载chunk的回调
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
// 将moreModules中的依赖添加至依赖缓存中(__webpack_modules__)
// 当所有的chunk加载完后执行入口文件(runtime)
var moduleId, chunkId, i = 0;
if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
for(moduleId in moreModules) {
if(__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if(runtime) var result = runtime(__webpack_require__);
}
if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]();
}
//加载完成后打上标记
installedChunks[chunkIds[i]] = 0;
}
return __webpack_require__.O(result);
}
// 获取webpackChunkbasic上的chunks列表,即依赖该文件的全局chunks列表
var chunkLoadingGlobal = self["webpackChunkbasic"] = self["webpackChunkbasic"] || [];
// 遍历全局chunks逐个执行加载回调
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
})();
// startup
// Load entry module and return exports
// This entry module is referenced by other modules so it can't be inlined
var __webpack_exports__ = __webpack_require__("./node_modules/conversion_radix/lib/index.js");
__webpack_exports__ = __webpack_require__.O(__webpack_exports__);
})();
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
结合index.bundle.js,可以得出进行代码分割后,共享模块会去收集所有依赖它的chunks并添加到自身_webapck_modules_
。这就好比倒过来了,入口文件成了依赖,而共享模块成了依赖收集者。
由于入口chunks是被添加到一个全局数组中,并没有立即执行。必须要等到共享模块加载完毕进行依赖收集才会触发入口chunks的代码执行,这样就确保了入口文件执行时所依赖的共享模块一定是加载完毕的。
webpack5的dependOn
支持依赖多个共享模块:
const config: webpack.Configuration = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: ['shared', 'tool']
},
other: {
import: './src/other.js',
dependOn: 'shared'
},
shared: 'conversion_radix',
tool: './utils/tool'
},
optimization: {
// 提取公共胶水代码
runtimeChunk: 'single',
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
依赖多个共享模块时必须要配置optimization.runtimeChunk
为single。不然index.bundle.js里收集的共享依赖会缺失,并且生成的共享模块每个文件里都会有上面shared.bundle.js中的那些胶水代码,而且index.bundle.js也会被收集两次。
配置里optimization.runtimeChunk
后,会多打包出一个runtime.bundle.js。
runtime.bundle.js中只包含胶水代码,而共享模块中则变为了与index.bundle.js一样被push到了runtime.bundle.js的全局数组中。
之前的shared.bundle.js中几段代码难以理解,例如:
// 获取webpackChunkbasic上的chunks列表,即依赖该文件的全局chunks列表
var chunkLoadingGlobal = self["webpackChunkbasic"] = self["webpackChunkbasic"] || [];
// 遍历全局chunks逐个执行加载回调
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
2
3
4
5
这里最后为什么要重写push呢?
通过runtime代码我们知道依赖收集的大致流程就是runtime加载完后遍历全局数组进行收集。那么问题来了,runtime只加载一次,如果其他依赖都晚于runtime加载完怎么办?
答案就是直接将push改写为依赖收集的方法,这样后加载的文件被push到全局数组时就会自动触发依赖收集,完美解决里加载顺序的问题。
另一段之前不太好理解的就是__webpack_require__.O
:
(() => {
var deferred = [];
__webpack_require__.O = (result, chunkIds, fn, priority) => {
// 记录异步的依赖以及依赖加载完的回调
if(chunkIds) {
priority = priority || 0;
for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) {
deferred[i] = deferred[i - 1];
}
deferred[i] = [chunkIds, fn, priority];
return;
}
var notFulfilled = Infinity;
// 通过__webpack_require__.0.j查找installedChunks是否存在已加载完的chunkId
for (var i = 0; i < deferred.length; i++) {
var [chunkIds, fn, priority] = deferred[i];
var fulfilled = true;
for (var j = 0; j < chunkIds.length; j++) {
if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
// 每加载完一个则移除
chunkIds.splice(j--, 1);
} else {
fulfilled = false;
if(priority < notFulfilled) notFulfilled = priority;
}
}
// 如果deferrd中的依赖都已加载则触发回调
if(fulfilled) {
deferred.splice(i--, 1);
var r = fn();
if (r !== undefined) result = r;
}
}
return result;
};
})();
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
这里其实是处理异步加载的文件。
例如当前index.bundle.js依赖tool和shared两个chunks,代码如下:
__webpack_require__ => { // webpackRuntimeModules
var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
__webpack_require__.O(0, ["shared","tool"], () => (__webpack_exec__("./src/index.js")));
var __webpack_exports__ = __webpack_require__.O();
}
2
3
4
5
可以看到,传递给self["webpackChunkbasic"]
的runtime
不是直接执行,而是调用__webpack_require__.o
,第三个参数才是执行函数。
第一次执行由于传入里chunkIds
,会讲当前参数都存入deferred中。第二次不带参数再次执行则走下半段逻辑会通过查找instlledChunks
中已加载模块来判断当前依赖是否全部加载完,如果为加载完则不会触发执行函数。
而每个依赖加载完又回触发一次判断:
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
...
return __webpack_require__.O(result);
}
2
3
4
最终所有依赖都加载完就会触发执行函数。
总结
最后以一张关系图作为总结: