js
// src/index.jsx
const Counter = lazy(() =>
import(/* webpackChunkName: "lazy_chunk" */ "./features/counter/counter.jsx")
);
先抛代码,相信大家都用过或者熟悉以上懒加载模块的使用,大多数朋友也都懂如何实现加载的,那我们就来确认下打包产物到底是什么,异步加载的包如何被主包消费的。带着问题一起来探索。
dynamic import
源代码
源代码如文章开头代码示例,魔法注释自行查看官网介绍。
webpack output 配置如下,代码简单,没做其他分包处理,打包结果中最终会包含 main.bundle.js
和 lazy_chunk.bundle.js
两个 js 文件和一个 index.html
文件。
js
// webpack.config.js
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].bundle.js",
},
plugins: [
new HTMLPlugin({ template: "./src/index.html" }),
// new MiniCssExtractPlugin 等...
],
};
lazy_chunk 构建产物
chunk.bundle.js
js
"use strict";
// 1. 在 self["webpackChunkdemoy"] 中增加一个 item
(self["webpackChunkdemoy"] = self["webpackChunkdemoy"] || []).push([
["lazy_chunk"], // 对应魔法注释中的 chunk name
{
// 模块 path 对应执行函数
/***/ "./src/features/counter/counter.jsx":
/***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
// 2. 导出 Counter
__webpack_require__.d(__webpack_exports__, {
default: () => /* binding */ Counter,
});
// 通过 __webpack_require__ 引入依赖
var MODULE_1__ = __webpack_require__(/*! react */ "dependence path");
// 还有很多很多 var MODULE_2...3...4...
// babel-react 等编译后的结果
function Counter() {
// react_jsx_runtime _jsxs、_jsx
}
},
},
]);
//# sourceMappingURL=lazy_chunk.bundle.js.map
在代码中我们注意到整个 chunk 就执行了一个操作,将构建结果 push 到 self["webpackChunkdemoy"]
中,那 push 之后做了什么呢,带着疑问我们到 main.bundle.js
中看看这个变量是什么,又是在什么时机加载 lazy_chunk
的,两者是如何配合并执行的。
如何消费 lazy_chunk
顺着线索一步步去找这些相关的函数是什么,做了哪些事?
self["webpackChunkdemoy"]
js
// main.bundle.js
// 1. 全局搜索仅此 2 个 `webpackChunkdemoy` 文本,因此只需观察 chunkLoadingGlobal
var chunkLoadingGlobal = (self["webpackChunkdemoy"] = self["webpackChunkdemoy"] || []);
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 2. 改写 push,原生 push 入参,并执行 webpackJsonpCallback。补充:二参是上一步 push 传入的 chunk 构建产物
chunkLoadingGlobal.push = webpackJsonpCallback.bind(
null,
chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
);
通过观察 chunkLoadingGlobal.push
的改写,我们又发现了新的线索 webpackJsonpCallback
👇🏻
webpackJsonpCallback
查看代码请点击展开
scss
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;
// 注意 installedChunks
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);
}
ok 如你所见,代码折叠了,是的,因为不好看懂。installedChunks
是执行逻辑之前定义的,所以我们回到主线入手,看看 main
中如何加载 lazy_chunk
、又是如何消费包内容的。
在整个流程中,会回到 webpackJsonpCallback
中,接下来要多留意 webpackJsonpCallback
中出现的关键词 installedChunks
。好,我们正式回到完整流程的开始。
main 中如何加载 lazy_chunk?
调用入口
找到调用 lazy_chunk
的逻辑,不难看出 __webpack_require__.e
将返回携带懒加载模块内容的 Promise,Promise 的生成依赖 keyof __webpack_require__.f
(ts 伪着凑合看)。
js
// main.bundle.js
const Counter = /*#__PURE__*/ (0, react__WEBPACK_IMPORTED_MODULE_1__.lazy)(() =>
// 1. 加载函数 __webpack_require__.e
__webpack_require__
.e(/*! import() | lazy_chunk */ "lazy_chunk")
.then(
__webpack_require__.bind(
__webpack_require__,
/*! ./features/counter/counter.jsx */ "./src/features/counter/counter.jsx"
)
)
);
__webpack_require__.e = (chunkId) => {
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);// 2. __webpack_require__.f[key] 处理 promises 并返回
return promises;
}, [])
);
};
通过搜索发现 __webpack_require__.f
下只有一个方法 __webpack_require__.f.j
,那就简单了,看看返回的 promises
被 push 了哪些值👇🏻
准备阶段
js
// main.bundle.js
// 🔧 判断模块是否已加载
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
}
// 1. 已加载过的 chunk
var installedChunks = {
main: 0, // 0 - 已成功加载状态,因为当前执行的就是 main,所以是 0,加载成功前的值是 [resolve, reject],下边__webpack_require__.f.j 中会讲
};
__webpack_require__.f.j = (chunkId, promises) => {
// JSONP chunk loading for javascript
// 2. 已加载过则直接取,不需要再次请求资源
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]); // 3. 正在请求资源中,不需要重新发起,返回上一次的 promise 即可
} else {
// 未加载过的
if (true) {
// all chunks have JS
// setup Promise in chunk cache
var promise = new Promise(
(resolve, reject) =>
(installedChunkData = installedChunks[chunkId] = [resolve, reject])
);
promises.push((installedChunkData[2] = promise));
// 4. 此时的状态 promises = [promise]
// installedChunkData = [resolve, reject, promise]
// installedChunkData = { main: 0, lazy_chunk: [resolve, reject]}
// 5. 准备完整的 url __webpack_require__.p 资源目录地址 __webpack_require__.u 根据 webpack config 中 `output.filename` 生成完整名称
var url = __webpack_require__.p + __webpack_require__.u(chunkId); // 该例子中为 file:///Users/xxx/Desktop/demo/dist/lazy_chunk.bundle.js
// create error before stack unwound to get useful stacktrace later
var error = new Error();
// 6. 在 script 标签加载完成或者加载失败后执行,【script 与第7步 __webpack_require__.l 有关】
var loadingEnded = (event) => {
if (__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
// 7. installedChunkData !== 0 加载失败
if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
if (installedChunkData) {
error.message = 'xxx' // 简化...
installedChunkData[1](error);
}
}
};
// 7. 加载函数
__webpack_require__.l(
url,
loadingEnded,
"chunk-" + chunkId,
chunkId
);
}
}
}
};
请求资源
轻松走到正式发起请求的时候了,那么这一步要做的就是 script 标签加载上一步生成的 url,成功则调用 installedChunks[chunkId][0] 中的 resolve
,并将 installedChunks[chunkId]
设置为0,失败则会执行 loadingEnded
中,执行 reject
。
js
var inProgress = {};// 1. 存储正在加载的资源
var dataWebpackPrefix = "demoy:";
__webpack_require__.l = (url, done, key, chunkId) => { // done - loadingEnded
// 已经在加载中的,绑定 done
if (inProgress[url]) {
inProgress[url].push(done);
return;
}
var script, needAttach;
// 避免重复添加 script 加载
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;
}
}
}
// 1. 创建 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;
}
// 2. 绑定 done
inProgress[url] = [done];
// 3. 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);// 5. 移除 script
doneFns && doneFns.forEach((fn) => fn(event)); // ⭐️ 6. 执行 done
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);
// ⭐️ 4. 插入执行,如果成功,此时已经立即执行该 js,也就是 lazy_chunk 中的 push - main 中的 webpackJsonpCallback
needAttach && document.head.appendChild(script);
};
我们看到上边这一步主要做了 2 件事,创建 script 标签执行js,即执行 webpackJsonpCallback
、执行 loadingEnded
。
后者我们已经在代码中注释过了,主要处理加载失败,执行installedChunks[chunkId][1]()
,即 __webpack_require__.e
中返回的 promise 的reject 函数,不记得的回去看一眼。
前者也就是我们从 lazy_chunk
推不过去的卡点 webpackJsonpCallback
,那么完成这一步整个链路就通了,话不多说👇🏻
执行并保存
回到 webpackJsonpCallback
js
__webpack_require__.m = __webpack_modules__;
// parentChunkLoadingFunction push data: [[chunkId], { moduleId: module }]
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
var moduleId,
chunkId,
i = 0;
// 1. 之前没有加载过,则以 moduleId 为颗粒度存储在 __webpack_modules__ 中
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);// 2. push 存储至 self["webpackChunkdemoy"]
// 3. resolve() 改变状态并将已加载 chunk 状态修改为 installedChunks[chunkId] = 0
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (
__webpack_require__.o(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
installedChunks[chunkId][0]();
}
installedChunks[chunkId] = 0;
}
};
webpack_modules 是什么,存储所有已加载模块信息的的变量。如果不清楚 module、chunk、asset 是啥关系,webpack.run
回调函数中取下 stats 参数查看。想查看每个 webpack 构建产物如何执行的,随便构建一个只有一行代码的项目,跑完方便观察模块的存取缓存如何设计的。
总结
看了这么多,抛开细节,主线剧情只有:
__webpack_require__
:取 by moduleId__webpack_mudules__
:存 moduleId:module function__webpack_require__.e
:异步取,返回promise
- 全文讲了啥 :
script
加载 chunk 返回,通过webpackJsonpCallback
将 chunk 中的 modules 存__webpack_mudules__
,通过 resolve 确定promise
状态,promise.then
通过__webpack_require__
从__webpack_mudules__
中取。
看图,虽然画很差劲。。。
最后,vscode 调试工具一如既往好用。