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'
    }
}
1
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__) => {
		// 源码省略
		...
	})
})()
1
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__);
})();
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

结合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',
    }
}
1
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));
1
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;
		};
  })();
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

这里其实是处理异步加载的文件。

例如当前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();
}
1
2
3
4
5

可以看到,传递给self["webpackChunkbasic"]runtime不是直接执行,而是调用__webpack_require__.o,第三个参数才是执行函数。

第一次执行由于传入里chunkIds,会讲当前参数都存入deferred中。第二次不带参数再次执行则走下半段逻辑会通过查找instlledChunks中已加载模块来判断当前依赖是否全部加载完,如果为加载完则不会触发执行函数。

而每个依赖加载完又回触发一次判断:

var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
			...
			return __webpack_require__.O(result);
		}
1
2
3
4

最终所有依赖都加载完就会触发执行函数。

总结

最后以一张关系图作为总结:

shared.png

ON THIS PAGE