有些文件一开始用不到,我们希望用到的时候再去加载,那么应该怎么做呢?其实 webpack 已经提供好了方法,我们可以通过import()语法,也可以通过require.ensure,当我们采用这种方法加载模块时候,就会发起一个 JSONP 请求,动态去加载模块,但是,有几个问题,比如,如何保证文件只加载一次,如何判断文件加载完成。
现在我们改一下主入口文件,将原来的add.js修改为通过 import 语法异步加载模块

异步文件加载

1
2
3
4
import('./add').then(data => {
console.log(data)
})

这个时候我们再来执行命令来查看输出文件,可以看到除了原来的 bundle 之外,还多出了一个src_add_js.bundle.js,而原来的bundle.js也多了不少的内容,由于代码过长,这里就不贴上来了,从入口开始分析异步加载

1
2
3
4
dist
|
|__bundle.js
|__src_add_js.bundle.js

入口函数同样为__webpack_require__("./src/main.js"),在这里和之前的有些不同
,可以看到,import语法已经被转换为了__webpack_require__.e

1
2
3
4
5
6
7
__webpack_require__.e("src_add_js")

.then(__webpack_require__.bind(__webpack_require__,"./src/add.js"))

.then(data => { console.log(data) })


可以看到,下面这里主要是通过installedChunks记录了当前安装模块的一些状态信息,undefined 为模块未加载,null表示预加载,Promise正在加载,0加载完成,如果已经存在installedChunkData中,表示正在加载,将Promise pushpromises数组中,否则创建一个Promise,同时添加到installedChunks记录下来,并且通过__webpack_require__.u__webpack_require__.p返回了完整的文件路径,将路径,chunkId,都传递给了__webpack_require__.l方法

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
 	(() => {
<!--定义一个静态属性f,初始值为空对象,方便挂载其他方法-->
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
})();

<!--定义j方法-->
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
// installedChunks记录已经安装的异步模块,undefined 为模块未加载,null表示预加载,Promise正在加载,0加载完成
var installedChunks = {
"main": 0
};
__webpack_require__.f.j = (chunkId, promises) => {
// 如果当前chunk存在,直接返回chunk的状态,否则返回undefined
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
// 正在加载中
promises.push(installedChunkData[2]);
} else {
if(true) {
// installedChunks = {
// main:0,
// src_add_js:[resolve,reject,promise]
// }
//
// installedChunkData = [resolve,reject,promise]
//
//
var promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);

// start chunk loading
// 拼接路径 file:///G:/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAwebpack/dist/src_add_js.bundle.js
// __webpack_require__.p 返回当前/dist/目录,__webpack_require__.u拼接返回src_add_js.bundle.js
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
var error = new Error();
var loadingEnded = (event) => {
if(__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
if(installedChunkData) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
installedChunkData[1](error);
}
}
};
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
} else installedChunks[chunkId] = 0;
}
}
};

看下__webpack_require__.l的实现,通过inProgress判断是否已经在加载,再判断当前的script标签中是否已经存在当前加载的模块,不存在的话则会创建一个script标签,url为绝对路径,插入到document.head中,当插入到页面中时候,会执行到对应chunk的代码

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
// url:file:///G:/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAwebpack/dist/src_add_js.bundle.js
// done:loadingEnded 函数
// key: chunk-src_add_js
// chunkId: src_add_js
var inProgress = {};
var dataWebpackPrefix = "webpack-demo:";
__webpack_require__.l = (url, done, key, chunkId) => {
if(inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if(key !== undefined) {
//判断是否已经加载
var scripts = document.getElementsByTagName("script");
for(var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
// 创建script标签
if(!script) {
needAttach = true;
script = document.createElement('script');

script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
// 加载完成script标签事件
var onScriptComplete = (prev, event) => {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if(prev) return prev(event);
}
;
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
// 插入到页面
needAttach && document.head.appendChild(script);
};

创建好script标签,会执行src_add_js.bundle.js的代码。

这里的self === window,可以看到执行了webpackChunkwebpack_demo中的push方法,那么,这个方法在哪里定义的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([["src_add_js"],{
"./src/add.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval("...");
}),
"./src/c.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
eval("...");
})
}]);

在最下方,在window上挂载了webpackChunkwebpack_demo赋值给了chunkLoadingGlobal并且将数组中的push方法修改为webpackJsonpCallback,也就是当执行push方法,会触发上面的webpackJsonpCallback函数,第一个参数是push函数,data解构出来chunkIds是所有的异步模块数组,moreModules是异步模块中引用的模块, 遍历chunkIds数组,找到installedChunks对象中保存的依赖的resolve函数,并且将installedChunks中的chunkId设置为 0,表示加载完成,通过遍历moreModules,加入到__webpack_modules__中,之后遍历执行resolves中,也就是所有installedChunks中的resolve函数,表示加载完成,加载完成之后,会执行加载完成事件,将script标签删除

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
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if(runtime) runtime(__webpack_require__);
if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
while(resolves.length) {
resolves.shift()();
}

}
// 修改push方法
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null,
// webpackJsonpCallback函数第一个参数是push函数
chunkLoadingGlobal.push.bind(chunkLoadingGlobal));