文章目录
介绍
Webpack 的异步导入(dynamic import)支持代码分割与懒加载。本文通过分析打包后的代码,来理解 webpack 在运行时如何实现异步模块加载。
打包后的运行时代码
模块系统核心
// 模块存储:将模块 ID 映射到模块定义函数
var modules = {};
// 模块缓存:保存已安装/已加载模块
var cache = {};
// 模块加载函数
function require(moduleId) {
// 若模块已加载,直接返回缓存
if (cache[moduleId]) {
return cache[moduleId];
}
// 创建并缓存新模块对象
var module = (cache[moduleId] = { exports: {} });
// 执行模块定义函数,填充 exports
modules[moduleId](module, module.exports, require);
return module.exports;
}
// 将模块存储和缓存挂载到 require 上
require.m = modules; // 模块定义
require.c = cache; // 已安装模块
模块定义辅助方法
// 将模块标记为 ES module
require.r = exports => {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
Object.defineProperty(exports, "__esModule", { value: true });
};
// 为 exports 定义 getter
require.d = (exports, definition) => {
for (var key in definition) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
};
Chunk 加载工具函数
// 加载 chunk 的基础 URL
require.p = "";
// 根据 chunk ID 生成文件名
require.u = chunkId => chunkId + ".main.js";
// 创建并注入 script 标签来加载 chunk
require.l = url => {
let script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
异步 Chunk 加载
// 跟踪已安装/下载中的 chunks
// 值为 0 表示已安装,值为 [resolve, reject] 表示加载中
var installedChunks = { main: 0 };
// 用于 chunk 加载的 JSONP 回调注册逻辑
require.f.j = (chunkId, promises) => {
var installedChunkData;
// 创建 promise,并保存 resolve/reject
var promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 将 promise 存入数组,供 Promise.all() 汇总
promises.push((installedChunkData[2] = promise));
// 发起 chunk 脚本加载
var url = require.p + require.u(chunkId);
require.l(url);
};
// 加载 chunk 并返回 promise
require.e = chunkId => {
let promises = [];
require.f.j(chunkId, promises);
return Promise.all(promises);
};
全局 Chunk 加载回调
// 处理已加载 chunk 数据的回调
var webpackJsonCallback = (chunkIds, moreModules) => {
var resolves = [];
// 取出并准备执行所有待完成 chunk 的 resolve
for (var i = 0; i < chunkIds.length; i++) {
var chunkId = chunkIds[i];
resolves.push(installedChunks[chunkId][0]); // 获取 resolve 函数
installedChunks[chunkId] = 0; // 标记为已安装
}
// 将新模块合并到模块存储中
Object.assign(modules, moreModules);
};
// 设置用于 JSONP chunk 加载的全局数组
var chunkLoadingGlobal = (window["webpackChunk_app_bundle"] = []);
chunkLoadingGlobal.push = webpackJsonCallback; // 重写 push 以触发回调
入口使用方式
// 10 秒后异步加载一个 chunk
window.setTimeout(() => {
require.e("src_video_js").then(require.bind(null, './src/video.js')).then(() => {
// chunk 加载完成,模块可通过 require() 获取
});
}, 10000);
懒加载 Chunk 的模块定义
// chunk 模块定义(通过 JSONP 方式加载)
window["webpackChunk_app_bundle"].push([
["src_video_js"],
{
"./src/video.js": (module, exports, require) => {
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORT,
});
var DEFAULT_EXPORT = "video";
},
},
]);
webpack异步加载流程总结
- 触发:调用
require.e(chunkId) - 创建 Promise:创建带
resolve/reject的 promise,并存入installedChunks - 注入脚本:创建带 chunk URL 的
<script>标签并插入 DOM - 执行 Chunk:浏览器加载并执行 chunk 脚本
- JSONP 回调:chunk 调用
window["webpackChunk_app_bundle"].push(),触发webpackJsonCallback - 模块注册:把新模块合并到
modules存储 - Promise 完成:待处理 promise 被 resolve,加载结束