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
  `)
}

每个文件被包裹进一个函数moduleexports__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 风格的模块化代码。

posted @ 2023-04-19 16:44  HuangBingQuan  阅读(102)  评论(0)    收藏  举报