webpack 打包产物分析
开发时,我们尽可能地进行模块化导入导出,在构建时,会把用到的模块给聚合成为一个 bundle.js (暂不考虑分包),今天探讨的问题是,这个bundle.js 的结构是什么样子
案例
1、创建工程:
mkdir webpackTest
cd webpackTest
npm init
pnpm add webpack webpack-cli2、项目结构
其中,add.js 如下
export function add(a, b) {
return a + b
}其中,main.js 如下
import { add } from "./src/add"
console.log('this is main')
let b = 500
const c = add(100, b)
console.log('c is ', c)其中,webpack.config.js 如下
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: "./main.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "js/main.bundle.js",
},
};3、构建
npx 会自动找到当前目录下面的 webpack.config.js 再和命令合并
npx webpack最终我们得到一个 main.bundle.js,这是一个 dev 模式下的产物,内容比较全
一点点分析,首先是顶部的注意事项:
在 dev mode 下使用了
eval,不用于生产也不用于查阅? 哦哦,用来驱动浏览器devTool去分割源码文件,如果要查阅的话换个devTool
查了一下官网,我们用 source-map 方案,便于调试
module.exports = {
...
devtool: 'source-map',
}OK,开始。
4、打包产物整体分析
结构上,首先这整个 webpackBootstrap 是一个立即执行的箭头函数。
内部分为这么几个部分:
__webpack_modules____webpack_module_cache____webpack_require__- runtime
- define property getters
- hasOwnProperty shorthand
- make namespace object
- startup 部分
a. __webpack_modules__
可以看到,__webpack_modules__ 是一个对象 key:就是我们项目的文件模块路径 value:一个函数,参数是 module exports require,这很难不让我们联想到了 CommonJS 的 require 实现,函数体是一个eval 函数。
我们两行写好的 add 函数被webpack包成了用 webpack 的 module\export\require 的方式,去除注释长这样。
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {add: () => (add)});
function add(a, b) {
return a + b
}
}我们的 add 函数还是在的,但是 import\export 这些 ESM 语法都无了,比较关键的是 __webpack_require__ 对象的 .r 方法和 .d 方法,后序有这个对象的定义。
b. __webpack_module_cache__
module_cache 暂时是一个空对象,在 __webpack_require__ 函数中有应用
c. __webpack_require__
__webpack_require__ 是一个函数,参数是模块 ID,返回值是模块的导出内容,这完全和 CJS 的 require 是类似的。
函数体中的注释很清晰:
- 检查这个模块是否加载过,加载过直接从
__webpack_module_cache__取 - 否则,创建 module 对象并加入缓存,初始化为一个带有 exports 属性的对象
- 执行模块函数,也就是从
__webpack_modules__表里取出模块函数并执行,参数就是module,module.exports,__webpack_require__
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}d. runtime 的三个方法
__webpack_require__本身是一个函数,又在这个函数对象上挂了属性 d, o, r,每个属性都是一个方法。

tips:之所以使用IIFE 而不是直接执行,是为了作用域隔离,比如 var key,防止作为全局作用域,IIFE的方式,让 var key 只在函数作用域中。
(1) __webpack_require__.d
用 defineProperty 把导出内容以 getter 的形式挂到 export 对象上
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};(2)__webpack_require__.o
其实就是 Object.prototype.hasOwnProperty.call 的简写
__webpack_require__.o =
(obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))(3)__webpack_require__.r
标记一个模块是否是 ESM 模块
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};用 Symbol.toStringTag 把一个对象的 Object.prototype.toString.call(obj) 结果变为 [object Module]
e. startup 部分
这部分是打包的 entry 部分,也是包在一个 IIFE 里(作用域隔离)
可以看到,main.js 中,esm 的 import 语句被替换了:
// old: import {add} from './src/add'
var _src_add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");我们的 add 方法也被替换成了这个 IMPORTED_MODULE 的 add 属性
// old: const c = add(100, b)
const c = (0,_src_add__WEBPACK_IMPORTED_MODULE_0__.add)(100, b)5、打包结果执行和使用
一般在项目代码打包结束后,我们有个 index.html 会用个 script 去引入我们的 js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器执行打包结果</title>
</head>
<body>
<script src="./js/main.bundle.js"></script>
</body>
</html>浏览器正常执行我们的代码,我们成功用 webpack 把一个模块化的项目打包进一个 bundle.js ,自动处理好了依赖关系并执行。 
由于我们添加了 source-map,浏览器还会加载 main.bundle.js.map 文件,在控制台展示了符合我们项目结构的源码文件,也支持调试。
开发阶段,我们可以用 source-map 模式去做调试 debug,生产模式的话就不需要 sourcemap了,尽可能轻量。
到此,我们成功实践了从 ESM 到向下兼容的 webpack_require 实现的模块化。
6、打包产物的执行逻辑分析
我们详细分析一下 bundle.js 的运行流程 
运行流程
整体是一个 webpackBootstrap 立即执行函数,自上而下执行。
定义 __webpack_modules__ 模块表 定义 __webpack_module_cache__ 缓存已获取的模块导出对象 定义 __webpack_require__ 函数,从模块表里获取并执行,把结果放入缓存。 定义 __webpack_require__.o 函数,判断 hasOwnProperty(obj, prop) 定义 __webpack_require__.d 函数,把模块导出内容挂到 export 对象上 定义 __webpack_require__.r 函数,核心是标记 ESM 定义 __webpack_exports__ = {}
启动!
- webpack_require.r(webpack_exports); ,把这个目前为空的对象标记为 ESM
__webpack_require__("./src/add.js"),调 用__webpack _require__函数- module_cache 中没有,先设为
{ exports: {} },然后执行__webpack_modules__["./src/add.js"](module, module.exports, __webpack_require__)
__webpack_require__.r(__webpack_exports__);// 标记export为ESM__webpack_require__.d(exports, {add: ()=> add});// 把 add 函数用 getter 访问器加到 exports 上,函数执行完毕- 回到
__webpack_require__函数,函数返回 module.exports,最终返会 exports 对象,也就是有 add 模块访问器的对象。(注意,因为module和缓存中是同一个引用,所以缓存中也存了一份 add.js 的 module) __webpack_require__执行完毕,结果被赋值到_src_add__WEBPACK_IMPORTED_MODULE_0__,然后就是执行剩余代码了。
总结
其实并不复杂,因为模块表已经有了
核心一直都是 module 对象以及 module 对象的 exports 属性。
我们维护的缓存是 <模块文件名>:<module对象> 的缓存
从入口文件开始,引入新模块时,就去表里找模块函数,在缓存表中设定一个空module: { exports: {} },然后把他传给模块函数,模块函数直接在 module.export 上设置getter访问器。
就这样一个过程而已........