Webpack 打包出来的 bundle 结构是怎样的?
Webpack 打包出来的 Bundle(bundle.js)本质上是一个 自执行函数(IIFE, Immediately Invoked Function Expression),它在浏览器端模拟实现了一套类似于 CommonJS / ES Module 的模块加载机制。
为了让你直观理解,我们可以将 Webpack 打包后的 bundle 结构拆解为 四大核心部分:
一、 Bundle 的总体骨架(以未压缩的开发模式为例)
简化后的 Bundle 结构如下:
javascript
/********************************************************************/
/**/ (function(modules) { // 1. 接收所有模块组成的字典/数组
/**/ // 2. 模块缓存对象
/**/ var installedModules = {};
/**/
/**/ // 3. Webpack 自己实现的 require 函数(核心 loader 逻辑)
/**/ function __webpack_require__(moduleId) {
/**/ // 如果模块在缓存中,直接返回
/**/ if(installedModules[moduleId]) {
/**/ return installedModules[moduleId].exports;
/**/ }
/**/ // 创建新模块对象并入库
/**/ var module = installedModules[moduleId] = {
/**/ i: moduleId,
/**/ l: false,
/**/ exports: {}
/**/ };
/**/
/**/ // 执行模块代码
/**/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/**/
/**/ // 标记模块已加载
/**/ module.l = true;
/**/
/**/ // 返回模块导出内容
/**/ return module.exports;
/**/ }
/**/
/**/ // 附加一些辅助方法(如处理 ES Module 兼容、异步加载等)
/**/ __webpack_require__.d = function(exports, name, getter) { ... };
/**/ __webpack_require__.r = function(exports) { ... };
/**/
/**/ // 4. 执行入口模块(Entry Point)并返回
/**/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/**/ })
/********************************************************************/
/**/ ({
// 5. 模块字典 (Module Map)
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
// 源码被编译后的代码...
}),
"./src/utils.js": (function (module, __webpack_exports__, __webpack_require__) {
// 源码被编译后的代码...
})
/****/ });
二、 四大核心组成部分详解
1. 模块字典(Modules Map)
- 形式:自执行函数的参数,通常是一个 Key-Value 对象(Webpack 5 中可能会优化为数组以提高性能)。
- Key:模块的唯一标识(通常是相对于项目根目录的路径,如
"./src/index.js",生产环境会被压缩为数字 ID)。 - Value:一个被匿名函数包裹的模块体。
- 作用:利用函数作用域隔离模块变量,防止全局污染。
- 传入参数:
module(当前模块对象)、exports(导出对象)、__webpack_require__(内部 require 函数)。
2. 模块缓存(Module Cache / installedModules)
- 形式:一个对象,例如
var installedModules = {}(Webpack 5 中为__webpack_module_cache__)。 - 作用:记录已经执行过的模块。
- 效果:当同一个模块被多次
import或require时,第二次会直接从缓存中读取exports,不会重复执行模块代码,同时也解决了循环引用的问题。
3. 核心加载器(__webpack_require__)
这是 Webpack 实现模块化的基石,它替代了原生的 require 或 import:
- 检查
moduleId是否在缓存中,在则直接返回。 - 不在缓存中,则初始化一个
module = { i: moduleId, l: false, exports: {} }。 - 从“模块字典”中找到对应的函数并执行,把
module.exports作为作用域上下文传进去。 - 模块内部通过修改
module.exports或exports来导出内容。 - 返回
module.exports。
4. 辅助函数(Helper Tools)
Webpack 会在 __webpack_require__ 上挂载很多工具方法,用于处理现代 JS 特性,比如:
__webpack_require__.r:给导出对象打上__esModule标记,表示这是一个 ES6 模块。__webpack_require__.d:通过Object.defineProperty的 getter 实现 ES Module 的动态绑定(Live Bindings)。__webpack_require__.e:处理代码分割(Code Splitting)和异步加载(import()),本质是通过创建<script>标签动态加载 JSONP 文件。
三、 源码转换对比(模块是如何被改写的?)
假设你的源码是 ES Module 规范:
原始源码 (./src/utils.js):
javascript
export const add = (a, b) => a + b;
被 Webpack 打包进 Bundle 后的代码:
javascript
"./src/utils.js": (function (module, __webpack_exports__, __webpack_require__) {
// 标记这是一个 ES Module
__webpack_require__.r(__webpack_exports__);
// 导出 add 变量
__webpack_require__.d(__webpack_exports__, "add", function() { return add; });
const add = (a, b) => a + b;
})
四、 Webpack 5 的 Bundle 结构变化
如果你使用的是 Webpack 5,打出的 bundle 结构会进一步优化:
- 更精简的 Runtime:不再强制生成包含所有逻辑的大 IIFE。Webpack 5 会根据你用到的功能(如 Tree Shaking、异步加载),按需生成 Runtime 代码。
- 顶层作用域清理:使用了更现代的 ES6+ 语法(如箭头函数、
const/let)替代传统的函数闭包,体积更小,执行更快。 - 更高效的模块标识:在生产模式(production)下,路径 ID 被彻底替换为极短的数字 ID( deterministic 策略)。
总结
Webpack 打包出来的 Bundle 就像一个自我包含的微型操作系统:
- 源码被拆分成一个个被函数包裹的模块(模块字典)。
- Runtime(运行时脚本)扮演“调度员”的角色,利用
__webpack_require__和 缓存机制,按照依赖图谱依次执行模块,最终启动整个应用。
右滑查看面试常问