webpack模块化原理解析(三)—— Code Splitting 原理

Code Splitting(代码分离)能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。对于动态加载模块,有两种常用的使用方式,第一种是import() 语法。第二种,则是使用webpack特定的require.ensure

鉴于 webpack 的强烈推荐import() 语法,这篇文章还是从一个使用import()语法的例子入手,剖析动态加载模块的原理。

一个简单的例子

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
import(/* webpackChunkName: "foo" */ './foo').then(({foo}) => {
console.log(foo);
})
import(/* webpackChunkName: "bar" */ './bar').then((bar, {bar1}) => {
console.log(bar(), bar1());
})

// foo.js
export const foo = 2

// bar.js
export default function bar() {
console.log('bar es Modules');
}
export function bar1() {
console.log('bar es Modules');
}

webpack 配置

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
var path = require("path");

module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'dist'),
filename: 'index.js',
chunkFilename: '[name].bundle.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
// dist/index.js
(function(modules) {
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0,
resolves = [],
result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0])
}
installedChunks[chunkId] = 0
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId]
}
}
if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
while (resolves.length) {
resolves.shift()()
}
};
var installedModules = {};
var installedChunks = {
2: 0
};

function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports
}
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
if (installedChunkData === 0) {
return new Promise(function(resolve) {
resolve()
})
}
if (installedChunkData) {
return installedChunkData[2]
}
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject]
});
installedChunkData[2] = promise;
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = "text/javascript";
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc)
}
script.src = __webpack_require__.p + "" + ({
"0": "foo",
"1": "bar"
}[chunkId] || chunkId) + ".bundle.js";
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;

function onScriptComplete() {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'))
}
installedChunks[chunkId] = undefined
}
};
head.appendChild(script);
return promise
};
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
})
}
};
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ? function getDefault() {
return module['default']
} : function getModuleExports() {
return module
};
__webpack_require__.d(getter, 'a', getter);
return getter
};
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
};
__webpack_require__.p = "";
__webpack_require__.oe = function(err) {
console.error(err);
throw err;
};
return __webpack_require__(__webpack_require__.s = 0)
})([(function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then(({
foo
}) => {
console.log(foo)
}) __webpack_require__.e(1).then(__webpack_require__.bind(null, 2)).then((bar, {
bar1
}) => {
console.log(bar(), bar1())
})
})]);

分析

可以看出,打包后的代码依旧是个 IIFE 函数,而且大体与 ES Module / CommonJs 打包代码无异,变化的是 webpack 异步加载代码的部分,重点在于__webpack_require__.e

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
__webpack_require__.e = function requireEnsure(chunkId) {

// 检查模块是否缓存
var installedChunkData = installedChunks[chunkId];
if (installedChunkData === 0) {
return new Promise(function(resolve) {
resolve()
})
}

// 判断是否加载中
if (installedChunkData) {
return installedChunkData[2]
}

// 缓存模块,存入 Promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject]
});
installedChunkData[2] = promise;

// 用 script 标签加载模块
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = "text/javascript";
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc)
}
script.src = __webpack_require__.p + "" + ({
"0": "foo",
"1": "bar"
}[chunkId] || chunkId) + ".bundle.js";

// 12s 再试,异常处理
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;

function onScriptComplete() {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'))
}
installedChunks[chunkId] = undefined
}
};
head.appendChild(script);
return promise
};

流程分析

  1. 检查模块chunkId是否缓存,如果installedChunkData缓存标志为0的话,则返回 resolve;如果installedChunkData为数组,则模块处于加载中。
  2. 如果模块没有缓存,创建promoise,将resolverejectpromise存入缓存。
  3. 创建script标签,src属性指向模块资源,标签append至 head标签,动态加载js 模块。
  4. 通过script.onerrorscript.onloadscript 标签做异常处理,若异常,则抛出错误,调用reject
  5. 最后返回promise 实例,实现import()

script 标签加载完毕,模块代码开始执行,所以我们来看看其中一个bundlefoo.bundle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
webpackJsonp([1],{

/***/ 2:
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["default"] = bar;
/* harmony export (immutable) */ __webpack_exports__["bar1"] = bar1;
function bar() {
console.log('bar es Modules');
}

function bar1() {
console.log('bar es Modules');
}

/***/ })

});

分析上述代码可知,webpackJsonp 的参数moreModules本质就是前两篇文章所说的模块数组,moreModules就不多加分析,关键的是webpackJsonp 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0,
resolves = [],
result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0])
}
installedChunks[chunkId] = 0
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId]
}
}
if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
while (resolves.length) {
resolves.shift()()
}
};

流程分析

  1. 循环chunkIds,取出installedChunksresolve,构建resolves数组
  2. 循环模块数组,将模块添加到缓存模块数组modules
  3. 循环调用resolves 数组,完成__webpack_require__.eimport()异步加载

总结

  • webpack 的动态加载模块,是通过__webpack_require__.e创建 script脚本资源, 通过webpackJsonp完成异步加载。
  • webpack 动态加载用到 promise API,对于不支持 promise 的浏览器,需要做对 promise 的 shim