在Webpack中常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用来分离代码。
动态导入也就是import()的形式;主要实现思路为使用script标签src的形式(jsonp)将js chunk脚本加载到, 然后挂载在全局的__webpack_modules__上,接着执行跟正常模块一样的require逻辑
在正常情况下,使用import函数加载一个模块的代码如下:
import("./src_click_js").then(module => {
module.default()
})经过webpack编译后,可以看到其实分为了2步
- 加载模块
- require模块 动态导入主要做的事情其实是第一步,正常模块的加载都是第2步
require.import("src_click_js").then(require.bind(require, "./src/click.js")).then(module => {
module.default()
}) // 这里的import返回的promises就是编译后代码的链式调用的promise
require.import = (chunkId) => {
const promises = []
require.jsonp(chunkId, promises)
return Promise.all(promises);
};jsonp 方法为当前异步加载的模块构造了一个[resolve, reject, promise]的结构,保存在全剧 当前文件加载的失败或者成功,都会取出来执行相应的resolve reject函数 伪代码:
require.jsonp = (chunkId, promises) => {
var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
// 为当前chunkId对应的模块实力恶化一个promise对象, 值是 [resolve, reject, promise]
promises.push(installedChunkData[2] = promise);
var url = require.prefix + require.uuid(chunkId);
var error = new Error();
var loadingEnded = (event) => {
if (require.ownProperty(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;
// 加载失败的话 调用 reject 函数
installedChunkData[1](error);
}
}
};
// 执行加载
require.load(url, loadingEnded, "chunk-" + chunkId, chunkId);
};伪代码:
require.jsonp = (url, done) => {
// 创建script标签=
script = document.createElement('script');
script.src = url;
var onScriptComplete = (prev, event) => {};
// 讲当前script添加到页面,这个时候,开启script默认的加载流程, 当加载完后
// 加载完的模块直接调用push方法 此push方法是被重写过的 self["webpackChunkmodule_test"].push方法
document.head.appendChild(script);
}至此,文件加载完,调用chunkLoadingGlobal.push方法,如下
self["webpackChunkmodule_test"] 其实就是一个数组,当加载异步模块的时候,记录着加载的模块的信息,格式如下:
[
["文件的id"],
{
"文件的id": ((module, export, require) => {
require.setModuleTag(__webpack_exports__);
require.definition(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
const __WEBPACK_DEFAULT_EXPORT__ = (() => {
console.log('按钮点击了')
});
})
}
]这一步非常关键,当异步加载模块后,立马执行的push方法就是这个被重写的push方法, 伪代码如下:
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
var moduleId, chunkId, i = 0;
if (chunkIds.some((id) => (installedChunks[id] !== 0))) {
for (moduleId in moreModules) {
if (require.ownProperty(moreModules, moduleId)) {
// 安装到全局的modules对象上
require.modules[moduleId] = moreModules[moduleId];
}
}
if (runtime) var result = runtime(require);
}
// 执行数组的默认的push方法
if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (require.ownProperty(installedChunks, chunkId) && installedChunks[chunkId]) {
// 执行resolve方法 代表此时加载结束 结束加载后 在执行require过程
installedChunks[chunkId][0]();
}
// 更新模块加载状态标记
installedChunks[chunkId] = 0;
}
}
var chunkLoadingGlobal = self["webpackChunkmodule_test"] = self["webpackChunkmodule_test"] || [];
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));执行完重写的push方法后,执行了resolve函数, 返回到编译代码的第二个then,继续require的过程,这个过程就跟普通的模块的require过程是一样的