Webpack 是怎么把多个文件打包成一个的
读完这篇,你会理解 webpack 打包产物的内部结构,以及模块系统在浏览器里是怎么运行的。
为什么写这篇
每天都在用 webpack,但你有没有好奇过:
- 我写了十几个 JS 文件,打包后浏览器是怎么跑起来的?
import/require浏览器本身不支持,webpack 是怎么处理的?- 同一个模块
require三次,为什么代码只执行一次?
这篇文章从一段模拟打包产物的代码出发,把这些问题一次说清楚。
先说结论
Webpack 打包做了三件核心的事:
| 做了什么 | 为什么 |
|---|---|
| 每个文件包裹成一个函数 | 作用域隔离,变量不污染全局 |
自己实现一个 require |
浏览器没有 require,webpack 自己造一个 |
| 缓存模块执行结果 | 同一模块多次 require 只执行一次 |
打包产物长什么样?
Webpack 打包后的文件,本质是一个立即执行函数(IIFE):
((modules) => {
// webpack 自己的运行时代码(__webpack_require 等)
// 执行入口模块,启动整个应用
__webpack_require("./src/index.js")
})({
// 所有模块打包在这里,key 是文件路径,value 是函数
"./src/a.js": function(module, exports, __webpack_require) { ... },
"./src/index.js": function(module, exports, __webpack_require) { ... },
})
结构分两部分:
- 里面:webpack 自己的运行时(加载模块、缓存等逻辑)
- 参数:业务代码,每个文件都变成了一个函数
每个文件是怎么变成函数的?
你写的原始代码:
// src/a.js
console.log('module a')
module.exports = 'a'
打包后变成:
"./src/a.js": function(module, exports, __webpack_require) {
eval(`
console.log("module a")
module.exports = "a"
//# sourceURL=webpack:///./src/a.js
`)
}
每个文件被包裹进一个函数,module、exports、__webpack_require 通过参数注入进来,完整模拟了 Node.js 的模块系统。
为什么用 eval?
开发模式下,//# sourceURL=webpack:///./src/a.js让浏览器 DevTools 知道这段代码来自哪个文件,报错和断点能正确定位。生产模式不用 eval,改用独立的 sourcemap 文件。
webpack 自己造了一个 require
这是整个打包产物的核心——__webpack_require:
var moduleExports = {} // 缓存已执行模块的结果
function __webpack_require(moduleId) {
// 第一步:检查缓存,有就直接返回
if (moduleExports[moduleId]) {
return moduleExports[moduleId]
}
// 第二步:找到该模块对应的函数
var func = modules[moduleId]
// 第三步:创建 module 对象(模拟 Node.js 的 module)
var module = { exports: {} }
// 第四步:执行模块函数,模块内部给 module.exports 赋值
func(module, module.exports, __webpack_require)
// 第五步:缓存执行结果
moduleExports[moduleId] = module.exports
// 第六步:返回导出内容
return module.exports
}
每次 require 一个模块,其实就是调用这个函数:找到对应的模块函数 → 执行 → 缓存 → 返回结果。
完整执行过程
以 index.js 引入 a.js 为例:
__webpack_require("./src/index.js")
│
▼
执行 index.js 对应的函数
│
├── 打印 "index module"
│
└── 调用 __webpack_require('./src/a.js')
│
▼
执行 a.js 对应的函数
│
├── 打印 "module a"
└── module.exports = "a" → 缓存,返回 "a"
│
▼
拿到 "a",打印 "a"
如果后续再次 require('./src/a.js'):
__webpack_require('./src/a.js') // 命中缓存 → 直接返回 "a",不再执行
__webpack_require('./src/a.js') // 命中缓存 → 直接返回 "a",不再执行
__webpack_require('./src/a.js') // 命中缓存 → 直接返回 "a",不再执行
最终控制台输出:
index module
module a
a
"module a" 只打印一次,无论 require 多少次。
为什么模块只执行一次?
这是 moduleExports 缓存的功劳:
第一次 require a.js → 没有缓存 → 执行函数 → 缓存结果
第二次 require a.js → 有缓存 → 直接返回,跳过执行
第三次 require a.js → 有缓存 → 直接返回,跳过执行
这个行为和 Node.js 的模块系统完全一致——模块是单例的,无论被引用多少次,初始化代码只跑一次。
整体架构总结
源码(多个文件)
↓ webpack 构建
依赖图(分析谁依赖谁)
↓
每个文件 → 包裹成函数
↓
所有函数 → 打包进一个 IIFE
↓
注入 __webpack_require(自制 require)
↓
bundle.js(浏览器可直接运行)
一句话理解 webpack
Webpack 不是魔法,它只是把每个 JS 文件变成一个函数,然后自己实现了一个带缓存的
require,让浏览器能运行 Node.js 风格的模块化代码。

浙公网安备 33010602011771号