Webpack 运行时代码分析 - ESM 静态导入模块

1,200 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 5 天,点击查看活动详情

文章内容:用 webpack 打包两个模块(通过 import 语法导入模块),分析打包后的代码,也就是 webpack 运行时代码。

代码准备

index.js

// const sum = require('./sum')
import sum, { test } from './sum';
import * as s from './sum';

console.log(sum(6, 9));
console.log("test", test);
console.log("s", s)

sum.js

const sum = (a, b) => {
  return a + b;
}

export default sum;
export const test = 'test';

webpack.config.js

const path = require('path')
module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'build')
  },
  mode: 'none'
}

在控制台执行

npx webpack

输出 build/main.js 文件

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__),
/* harmony export */   "test": () => (/* binding */ test)
/* harmony export */ });
const sum = (a, b) => {
  return a + b;
}

/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const test = 'test';


/***/ })
/******/ 	]);
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/
/******/ 	// The require function
/******/ 	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;
/******/ 	}
/******/
/************************************************************************/
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// 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] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
console.log("s", _sum__WEBPACK_IMPORTED_MODULE_0__)
})();

/******/ })()
;

然后就可以直接断点调试 build/main.js 啦~

运行时代码分析

首先与不包含 ESM 的产物代码(这里用的是《分析一个极简的 Webpack 运行时代码》文章中的产物代码)做对比

标红的部分是 webpack 为处理 ESM 模块所增加的处理逻辑

分析变量与函数

  • __webpack_modules__: 是一个数组,存放所有加载到的模块。
  • __webpack_module_cache__: 是一个对象,对模块执行结果进行缓存,这样能够保证每个模块只被执行一次;
  • __webpack_require__: 是一个函数,作用是加载模块,如果模块第一次被加载,则通过 __webpack_modules__[moduleId] 匹配上对应的模块,并进行缓存;如果是已加载的模块,则直接从 __webpack_module_cache__[moduleId] 取;
  • __webpack_require__.d: 定义 getter 方法
  • __webpack_require__.o: Object.prototype.hasOwnProperty
  • __webpack_require__.r: 注入 ESM 标注属性
  • __webpack_exports__: 是一个对象,存放导出的内容;

__webpack_require__.r

注入 ESM 标注属性

/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();

代码解析:

  1. 在函数体内部,首先通过检查环境中是否存在 Symbol 并且 Symbol.toStringTag 是否可用,来判断是否可以使用 toStringTag 属性。这一部分代码可以说是对环境的兼容性检测。
  2. 如果环境中存在 Symbol 并且 Symbol.toStringTag 可用,那么就会调用 Object.defineProperty 方法,在 exports 对象上定义 Symbol.toStringTag 属性,属性值为 'Module'这样做可以使得该模块在被打印或转换为字符串时能够显示为 '[object Module]'
  3. 不管上述条件是否成立,都会调用 Object.defineProperty 方法,在 exports 对象上定义 __esModule 属性,属性值为 true。这个属性是为了表示该模块是一个 ES6 模块,并且在其他模块引入时可以进行相应的处理。

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

引自 MDN-Symbol.toStringTag

Object.prototype.toString.call(exports) // '[object Module]'

__webpack_require__.d

/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// 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] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
  • 定义 getter 方法:遍历 definitionkey,如果 definition 有这个 key 而 exports 没有,则在 exports 的属性 key 上挂载 definition[key] 这个 getter 方法;
  • getter 方法的目的是:在访问某个导出特性的时候才去计算对应的值;
  • 这里的 definition 是传入的对象:
{
  "default": () => (__WEBPACK_DEFAULT_EXPORT__),
  "test": () => (/* binding */ test)
}

sum.js 源码与 webpack 运行时代码做对比分析

sum.js 源码

const sum = (a, b) => {
  return a + b;
}

export default sum;
export const test = 'test';

webpack 运行时代码中的 sum 模块

/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__),
/* harmony export */   "test": () => (/* binding */ test)
/* harmony export */ });
const sum = (a, b) => {
  return a + b;
}

/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (sum);
const test = 'test';


/***/ })
/******/ 	])

通过 __webpack_require__.r 标记 __webpack_exports__为 ESM 模块,然后通过 __webpack_require__.d__webpack_exports__defaulttest 导出特性定义 getter 方法。

对整个过程进行分析

var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
console.log("s", _sum__WEBPACK_IMPORTED_MODULE_0__)
})();
  • 加载入口模块 index.js;

__webpack_require__.r(__webpack_exports__);

  • 将 index.js 标记为 ESM


var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

  • 加载 sum 模块

    • 如果缓存中存在,则在缓存中取
    • 将 sum 模块标记为 ESM
    • 为 sum 模块的导出特性定义 getter 方法
  • 将结果赋值给 _sum__WEBPACK_IMPORTED_MODULE_0__


console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(6, 9));
console.log("test", _sum__WEBPACK_IMPORTED_MODULE_0__.test);
  • 调用 default 对应的 getter 方法
  • 调用 test 对应的 getter 方法

小结

本文主要对 webpack 打包含有 ESM 模块的运行时代码进行了分析,主要做了如下一些事情:

  • 用 webpack 提供的模块加载函数加载入口模块;

  • 加载入口模块所依赖的模块;

    • 如果缓存中存在,则在缓存中取
    • 将 import 导入的模块标记为 ESM(__esModule
    • 为模块导出特性定义 getter 方法
  • 运行模块中的代码。