Webpack 的动态导入(Dynamic Import)通过将 import()
语法转换为 __webpack_require__.e
实现异步加载模块。以下从源码角度解析其实现机制,并附加案例说明。
一、import()
语法转换过程
1. 解析阶段:识别 import()
Webpack 使用 Parser
解析代码,当遇到 import()
语法时,会触发 ImportParserPlugin
插件:
javascript
// lib/dependencies/ImportParserPlugin.js
class ImportParserPlugin {
apply(parser) {
parser.hooks.importCall.tap("ImportParserPlugin", expr => {
// 创建 ImportDependency
const dep = new ImportDependency(
expr.source.value,
expr.range,
parser.state.module
);
parser.state.current.addDependency(dep);
return true;
});
}
}
ImportDependency
表示模块的动态导入依赖,记录模块路径和代码位置。
2. 代码生成阶段:生成 __webpack_require__.e
在生成阶段,ImportDependency.Template
将 import()
转换为运行时代码:
javascript
// lib/dependencies/ImportDependency.js
class ImportDependencyTemplate extends ModuleDependency.Template {
apply(dependency, source, runtime) {
const dep = /** @type {ImportDependency} */ (dependency);
// 转换为 __webpack_require__.e(chunkId).then(...)
source.replace(
dep.range[0],
dep.range[1] - 1,
runtime.moduleNamespacePromise({
chunk: dep.chunkId,
request: dep.request,
})
);
}
}
runtime.moduleNamespacePromise
生成调用__webpack_require__.e
的代码。
二、__webpack_require__.e
源码解析
__webpack_require__.e
的核心实现在 EnsureChunkRuntimeModule
中:
javascript
// lib/runtime/EnsureChunkRuntimeModule.js
class EnsureChunkRuntimeModule extends RuntimeModule {
generate() {
return `
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
`;
}
}
- 核心逻辑 :通过
__webpack_require__.f
中的加载方法(如 JSONP)加载指定 chunk。
Chunk 加载(以 JSONP 为例)
JSONP 加载逻辑在 JsonpChunkLoadingRuntimeModule
中:
javascript
// lib/web/JsonpChunkLoadingRuntimeModule.js
generate() {
return `
var installedChunks = { main: 0 };
__webpack_require__.f.j = (chunkId, promises) => {
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 表示已加载
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
var promise = new Promise((resolve, reject) => {
installedChunkData = [resolve, reject];
});
installedChunks[chunkId] = [resolve, reject, promise];
promises.push(promise);
// 创建 script 标签加载 chunk
var url = __webpack_require__.p + chunkId + ".js";
var script = document.createElement('script');
script.src = url;
script.onerror = (event) => {
reject(new Error('加载失败: ' + (event && event.message)));
cleanup();
};
document.head.appendChild(script);
}
}
};
`;
}
installedChunks
跟踪 chunk 加载状态:0
(已加载)、Promise
(加载中)、undefined
(未加载)。- 错误处理 :
script.onerror
捕获加载失败,触发reject
。
三、Chunk 加载与错误处理
1. 加载成功
- 加载完成后,全局回调
webpackJsonpCallback
会标记 chunk 为已加载:
javascript
window["webpackJsonp"] = (chunkId, moreModules) => {
for (const moduleId in moreModules) {
__webpack_modules__[moduleId] = moreModules[moduleId];
}
installedChunks[chunkId][0](); // Resolve Promise
installedChunks[chunkId] = 0; // 标记为已加载
};
2. 错误处理
script.onerror
:处理网络错误或资源不存在。- 超时处理:可通过自定义逻辑添加超时检测:
javascript
setTimeout(() => {
reject(new Error('加载超时'));
cleanup();
}, 120000); // 2分钟超时
四、案例演示
源代码
javascript
// src/index.js
document.getElementById('btn').addEventListener('click', () => {
import('./module.js')
.then(module => module.showAlert())
.catch(err => console.error('加载失败:', err));
});
打包后代码(简化)
javascript
// dist/main.js
__webpack_require__.e("src_module_js").then(function() {
var module = __webpack_require__("./src/module.js");
return module;
}).then(module => module.showAlert()).catch(err => console.error('加载失败:', err));
// JSONP 加载 chunk
// dist/src_module_js.js
window["webpackJsonp"](["src_module_js"], {
"./src/module.js": (() => { /* module 内容 */ })
});
执行流程
- 用户点击按钮触发
__webpack_require__.e("src_module_js")
。 - 创建 Promise 并加载
src_module_js.js
。 - 加载成功后,调用
webpackJsonpCallback
注册模块并 Resolve Promise。 - 执行
.then
中的逻辑,若加载失败则进入.catch
。
五、总结
import()
转换 :通过ImportParserPlugin
和ImportDependency
转换为__webpack_require__.e
。- Chunk 加载 :利用 JSONP 动态加载脚本,通过
installedChunks
管理状态。 - 错误处理 :通过
script.onerror
和 Promise Rejection 捕获错误。