Webpack 中 Dynamic Import 打包产物剖析
2023-12-18 22:31:26
懒加载和模块化开发的思想已经渗透在每个前端开发人员的脑海中,在实际的项目开发中也确实给我们带来了非常大的好处,提升了加载效率和性能。webpack 作为模块化打包工具也完美地支持了动态加载模块。本文将带你了解 webpack 中动态加载模块时,打包产物究竟是怎样的,深入揭秘动态加载包是如何被主包消费的。
前言
在webpack 4.0 之前的版本中,由于不支持动态导入语法的标准,所以webpack 使用 commonjs 的异步加载方式,创建动态导入。不过在webpack 4.0 之后,webpack 也就内置了对 ES 模块标准的动态 import 语法支持。今天我们就将借助新版本的webpack 来了解webpack 中 Dynamic Import 的工作原理和实现方式。
首先让我们看看 Webpack 中 Dynamic Import 语法的简单应用:
const button = document.getElementById('btn');
button.addEventListener('click', () => {
import('./module.js').then(({ default: module }) => {
module();
});
});
这段代码使用了 ES 模块标准的动态 import 语法,在点击按钮时,动态加载 module.js 模块。
剖析打包产物
我们使用 webpack 对上述代码进行打包,并将打包后的代码通过代码格式化工具进行美化,就可以看到打包后的产物了:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.dynamicImport = {}));
}(this, (function (exports) { 'use strict';
// chunk 包依赖的代码
var CHUNK_DEPENDENCIES = [[0]];
// 模块对象
var modules = {
0: function(module, exports) {
console.log('module.js');
}
};
// 确保 module 的执行
function ensure(chunkId) {
var chunk = self.installedChunks[chunkId];
if (chunk === undefined) {
throw new Error("Cannot find chunk: " + chunkId);
}
if (chunk.promise) {
return chunk.promise;
}
if (chunk.modules.length === 0) {
chunk.modules.push(0);
}
chunk.isLoading = true;
var resolve = chunk.resolve;
chunk.promise = new Promise(function(resolve) {
resolvedChunks.push(resolve);
loadChunkById(chunkId);
});
chunk.result = [];
resolvedChunks.push(function() {
chunk.result = modules[chunk.id](chunk.module, chunk.exports);
chunk.isLoading = false;
resolvedModules.push(chunk);
});
}
// 通过 chunkId 加载 chunk
function loadChunkById(chunkId) {
if (installedChunks[chunkId]) {
return Promise.resolve();
}
requestedChunks[chunkId] = true;
// chunk 文件的 url
var url = self.p + chunkId + ".js";
var timeout = self.h();
var script = document.createElement('script');
var onScriptComplete = function(event) {
// avoid mem leaks - cleanup removes onload and
// detach element
script.onload = script.onerror = null;
clearTimeout(timeout);
var chunk = self.installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk.onerror = chunk.onload = null;
}
loadedChunks.unshift(chunk);
var promises = resolvedChunks;
resolvedChunks = [];
for (var i = 0; i < promises.length; i++) {
promises[i]();
}
}
};
var timeoutId = setTimeout(function() {
onScriptComplete({ type: 'timeout', target: script });
}, timeout);
script.onload = script.onerror = onScriptComplete;
script.timeout = timeoutId;
script.src = url;
head.appendChild(script);
}
var head = document.getElementsByTagName('head')[0];
var installedModules = {};
// 定义变量,记录动态加载的 chunk
var dynamicImports = [];
self.webpackJsonp.modules = modules;
self.webpackJsonp.modulesLoaded = [];
self.webpackJsonp.require = function(chunkId) {
var chunk = self.installedChunks[chunkId];
if (!chunk) {
throw new Error("Cannot find chunk: " + chunkId);
}
if (chunk.id === 0) {
return chunk.exports;
}
installedModules[chunkId] = true;
return chunk.exports;
};
self.webpackJsonp.load = function(chunkId) {
if (installedModules[chunkId]) {
return Promise.resolve();
}
// 非首包
chunk.modules = [].slice();
for (var j = 0; j < chunk.modules.length; j++) {
var moduleId = chunk.modules[j];
modules[moduleId] = module;
}
// 添加异步 chunk
dynamicImports.push(chunk);
// 创建首个动态 chunk
createChunk(chunkId);
loadChunkById(chunkId);
};
// ...省略代码
})));
通过上面的代码,我们可以看到以下几个关键点:
- chunk 包依赖的代码: 该数组记录了 chunk 所依赖的其他 chunk 的 ID。
- 模块对象: 该对象存储了模块的代码。
- 确保 module 的执行: 该函数用于确保模块被执行。
- 通过 chunkId 加载 chunk: 该函数用于通过 chunkId 加载 chunk。
- 动态加载的 chunk: 该数组记录了动态加载的 chunk。
动态加载包是如何被主包消费的
在前面的例子中,我们使用动态 import 语法加载了 module.js 模块。在 webpack 的打包产物中,module.js 模块被放在了一个单独的 chunk 中。当主包加载完成时,会触发 load 事件。在 load 事件中,会调用 loadChunkById 函数加载 module.js 模块所在的 chunk。加载完成后,会触发 onScriptComplete 事件。在 onScriptComplete 事件中,会调用 ensure 函数,确保 module.js 模块被执行。
需要注意的是,动态加载的 chunk 是异步加载的。这意味着在动态加载的 chunk 加载完成之前,主包是不会执行的。所以,在使用动态 import 语法时,需要确保在动态加载的 chunk 加载完成之前,主包中的代码不会执行任何依赖于动态加载的 chunk 的代码。