在 webpack 中使用import()方法可以动态加载ES6模块,示例如下:
// index.js
import('./sum').then(m => {
setTimeout(() => {
console.log(m.default(1, 2))
}, 3000)
})
// sum.js
const sum = (...args) => args.reduce((x, y) => x + y, 0)
export default sum
仍是写一个打包脚本build.js
const webpack = require('webpack')
const path = require('path')
function f1() {
return webpack({
entry: './index.js',
mode: 'none',
output: {
filename: 'main.[contenthash].js', // 文件hash
chunkFilename: '[name].chunk.[contenthash].js', // 模块hash
path: path.resolve(__dirname, 'dist/contenthash'), // 输出位置
clean: true // 输出之前清空
}
})
}
f1().run((err, stat) => {
console.log(stat.toJson())
})
使用webpack打包
node build.js
创建一个index.html文件引入dist/contenthash中都main.js文件。
直接看main.js末尾,如下
__webpack_require__.entry(/* import() */ 1)
.then(__webpack_require__.bind(__webpack_require__, 1))
.then(m => {
console.log(m.default(1, 2))
})
真实编译出来的是
__webpack_require__.e,我这里为了方便好看,将e替换成了entry
很明显,这段代码能看得出来,code spliting是通过promise实现的。我们在浏览器中跑一下看一下webpack 是如何做到的。
首先进入__webpack_require__.entry函数
__webpack_require__.entry = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
这里通过执行__webpack_require__.f中的属性,得到一个 promise 数组promises,然后调用Promise.all执行这个promises最后返回。
其中__webpack_require__.f内有一个属性j,j是一个函数,这里会执行这个j函数,函数如下
// 使用installedChunks对象来存储已加载和正在加载的模块
// 其中 undefined 表示 模块未加载,null 表示模块 preloaded/prefetched
// [resolve, reject, Promise] 表示加载中, 0 = 已加载
var installedChunks = {
0: 0
};
__webpack_require__.f.j = (chunkId, promises) => {
// JSONP chunk loading for javascript(这里应该很明显,JavaScript文件是通过JSONP加载的)
var installedChunkData = __webpack_require__.hasOwnProperty(installedChunks, chunkId)
? installedChunks[chunkId]
: undefined;
if (installedChunkData !== 0) { // 0 means "already installed". 如果是0表示已经加载过了
// a Promise means "currently loading".
if (installedChunkData) { // 如果存在,则是个 Promise,正在加载中
promises.push(installedChunkData[2]);
} else {
if (true) { // all chunks have JS
// setup Promise in chunk cache
// 创建一个 Promise,同时将 installedChunkData(当前安装模块) 与 installedChunks(记录安装的模块)[chunkId] 设置为 [resolve, reject]
var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
// 接着将这个 promise 对象添加到 promises内,同时将其添加到 installedChunkData, 此时 installedChunkData = [resolve, reject, promise]
promises.push(installedChunkData[2] = promise);
// start chunk loading
// 拼接 script 的 url(将要加载的js文件路径)
var url = __webpack_require__.path + __webpack_require__.urlFilename(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
var loadingEnded = (event) => {
if (__webpack_require__.hasOwnProperty(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);
}
}
};
// 调用 load,传入 url, 加载完成函数, key 和 chunkId
__webpack_require__.load(url, loadingEnded, "chunk-" + chunkId, chunkId);
} else installedChunks[chunkId] = 0;
}
}
};
这里有一个installedChunks对象来存储已加载和正在加载的模块。我们关注以下几个值状态:
- ** undefined 表示模块未加载**。
- [resolve, reject, Promise] 表示加载中。
- 0 表示已加载。
在__webpack_require__.f.j中的执行流程如下:
- 首先定义变量
installedChunkData,它的值会尝试在installedChunks中根据chunkId获取。 - 如果
installedChunkData不为0,则表示该模块要么还未加载(undefined),要么还在加载中([resolve, reject, Promsie]) - 如果模块已经在加载中,则将
installedChunkData[2](也就是那个 Promise)添加到promises中即可 - 如果模块未加载,则创建一个
promise,同时将installedChunkData(当前安装模块) 与installedChunks[chunkId](记录安装的模块)设置为[resolve, reject] - 接着将这个 promise 对象添加到
promises内,同时将其添加到installedChunkData, 此时installedChunkData的值为[resolve, reject, promise] - 直接看
Line47调用load方法,这个方法最终就是创建一个script然后加载模块js文件,加载完成后删除这个script
到这一段,sum.jsjs是加载出来了,但是并不能直接使用,也就是说加载script以后并没完,script加载以后里面还会做一些事情,我们来看一下加载的script会做什么操作。
"use strict";
(self["webpackChunk"] = self["webpackChunk"] || []).push(
[
[1],
[
/* 0 */,
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
const sum = (...args) => args.reduce((x, y) => x + y, 0)
const __WEBPACK_DEFAULT_EXPORT__ = (sum);
})
]
]
);
在其内部有一个self["webpackChunk"].push操作,我一开始压根就没注意到这个push函数,调试了半天,走了很多歪路~
后来我就在main.js中翻了一下,果然让我找到了webpackChunk这个属性,好家伙,原来此push非彼push啊
// install a JSONP callback for chunk loading
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;
// 解构出来 chunkIds 判断是否未被加载过
if (chunkIds.some((id) => (installedChunks[id] !== 0))) {
for (moduleId in moreModules) {
if (__webpack_require__.hasOwnProperty(moreModules, moduleId)) {
// 将 sum.js 中的那个函数拿过来,放到 moduels 上
__webpack_require__.modules[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__.hasOwnProperty(installedChunks, chunkId) && installedChunks[chunkId]) {
// installedChunks[chunkId] 就是之前的 [resolve, reject, promise]
// 这里就是 resolve 掉(代表着一开始 __webpack_require__.entry 中的 promise.all)
installedChunks[chunkId][0]();
}
installedChunks[chunkId] = 0; // 同时标记为已加载过
}
}
var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
重点是Line28``push不是数组的push,原来sum.js中调用的就是就是webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
进入到webpackJsonpCallback内部执行流程如下:
- 第二个参数
data就是sum.js中push过来的数组,解构出来的chunkIds需要判断是还未加载过的 - 将
sum.js中的函数赋值给__webpack_require__.modules - 最后是将
installedChunks中对应的模块状态中的resolve掉,同时标记模块已加载。 - 到这里,标志着
__webpack_require__.entry中的Promise.all执行完毕 - 到第一个
then中执行__webpack_require__(1),这其中的逻辑与我上一篇 Webpack 是如何打包CJS的?中一致,我这里就不赘述了,在这个地方其实就是返回一个对象,里面有一个default属性,default的值就是export default的sum函数。 - 最后我们就可以在最后一个
then函数中执行sum函数了,也就是m.default(1, 2)
总结
这里其实最重要的就是如下几个步骤:
- 创建状态,添加到
installedChunks中 - 通过创建
script标签,加载sum.js到head标签中,加载完成后删除这个script - 接着在
sum.js中调用webpackChunk.push方法 - 在
webpackChunk.push中将installedChunks中对应的模块状态resolve掉,同时状态设置为已加载 - 最后执行
__webpack_require__得到一个对象,内有属性default,default的值就是sum函数体。
呼~我这两篇文章都是看了B站UP 山月的视频 然后自己一步一步调试写出来的,这种产物代码实在是有点儿读懂,算是下了一番功夫,也很有收获,希望你也能按照步骤来调试看看。