Skip to content

webpack 打包产物分析

开发时,我们尽可能地进行模块化导入导出,在构建时,会把用到的模块给聚合成为一个 bundle.js (暂不考虑分包),今天探讨的问题是,这个bundle.js 的结构是什么样子

案例

1、创建工程:

mkdir webpackTest
cd webpackTest
npm init 
pnpm add webpack webpack-cli

2、项目结构

其中,add.js 如下

js
export function add(a, b) {
    return a + b
}

其中,main.js 如下

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 如下

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 方案,便于调试

js
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 的方式,去除注释长这样。

js
(__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__
js
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 对象上

js
// 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 的简写

js
__webpack_require__.o = 
(obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))

(3)__webpack_require__.r

标记一个模块是否是 ESM 模块

js
__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 语句被替换了:

js
// old: import {add} from './src/add'

var _src_add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");

我们的 add 方法也被替换成了这个 IMPORTED_MODULEadd 属性

js
// old: const c = add(100, b) 

const c = (0,_src_add__WEBPACK_IMPORTED_MODULE_0__.add)(100, b)

5、打包结果执行和使用

一般在项目代码打包结束后,我们有个 index.html 会用个 script 去引入我们的 js

html
<!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访问器。

就这样一个过程而已........