Skip to content
On this page

总结

  1. 虽然现代主流浏览器已支持 ESM,但在部分老旧的浏览器仍不支持,为了向下兼容,webpack 仍然会将 ESM 转化为 CommonJS,并注入到运行时代码。
    其实代码实现上,也不是转换成 CJS,而是转换为浏览器可以识别的更早规范 ES5(语言层级的规范,浏览器支持程度很高,不支持的都基本被淘汰了)。只是代码实现形式上很贴合 CJSES5 的实现,在 CJS 转换 ES5 的基础上添加代码去实现 ESM 的特性。
  2. __webpack_require__.r 标记其为 ESM 模块 (暂时没找到 r 的单词)
  3. __webpack_require__.d 定义其所有导出的值 (define)
  4. __webpack_require__.o 判断是否实例的属性 (ownProperty)
  5. __webpack_require__CJS 的运行时一致,意味着执行的传参和 CJS 的运行时代码一致,依旧还是 module、module.exports、require。且模块返回还是 module.exports
  6. 执行的传参和 CJS 的运行时代码一致,只不过 CJS 是为 module.exports 添加数据属性或直接赋值为堆里的另一个引用地址。而 ESM 是为其添加访问器属性 getter。
  7. module_wrapper 包裹函数内,对 module.exports 使用 __webpack_require__.r 进行标记,表示该模块已经处理过,且使用 __webpack_require__.d 定义其所有导出的访问器属性

提问

  • [x] 对含 ESM 模块的 webpack 运行时代码进行调试与理解

  • [x] webpack 含 ESM 的运行时代码做了那些事情
    主体框架与 CJS 运行时代码一致。

    1. 利用 AST 生成所有模块数组 __webpack_modules__,内部都是包裹函数。
    2. 生成缓存对象 __webpack_module_cache__
    3. 实现模块加载器 __webpack_require__,可以利用 __webpack_module_cache__ 读取缓存,或直接从所有模块数组 __webpack_modules__ 中取得模块执行并缓存
    4. 入口模块执行,即 0 号模块执行。

    区别在于,模块加载器 __webpack_require__ 函数扩展了以下三个方法:(在函数上扩展属性,目的是为了封装,且避免和模块内变量重名)

    1. __webpack_require__.r 标记其为 ESM 模块
    2. __webpack_require__.d 定义其所有导出的值 (define)
    3. __webpack_require__.o 判断是否实例的属性 (ownProperty)

    且每个模块,即每个包裹函数内,对 module.exports 使用 __webpack_require__.r 进行标记该模块已经处理过,且使用 __webpack_require__.d 定义其所有导出的访问器属性

  • [x] webpack_require 中的 d/r/o 各个变量指什么意思

  1. __webpack_require__.r 标记其为 ESM 模块
  2. __webpack_require__.d 定义其所有导出的值 (define)
  3. __webpack_require__.o 判断是否实例的属性 (ownProperty)

1. 前提提要、场景

虽然现代主流浏览器已支持 ESM,但在部分老旧的浏览器仍不支持,为了向下兼容,webpack 仍然会将 ESM 转化为 CommonJS,并注入到运行时代码。
其实代码实现上,也不是转换成 CJS,而是转换为浏览器可以识别的更早规范 ES5(语言层级的规范,浏览器支持程度很高,不支持的都基本被淘汰了)。只是代码实现形式上很贴合 CJSES5 的实现,在 CJS 转换 ES5 的基础上添加代码去实现 ESM 的特性。

webpack 是如何将 ESM 转化为 CommonJS 的,学习这个转换过程,有利于理解 ESM 的特性及其实现。

2. 导出转换

ESM 中,有 命名导出 和 默认导出 两种的方式,但在 CJS 中只有一种。
所以需要进行转换

  • default 转化为 module.exports.default
  • name 转化为 module.exports.name
js
// esm 代码
import sum, { name } from './sum'
import * as s from './sum'

// 转化后的 cjs 代码
const s = require('./sum')
const sum = s.default
const name = s.name

3. 运行时代码分析

js
/* webpack/runtime/define property getters */
(() => {
    // define getter functions for harmony exports
    // 定义 get 函数,实现 ESM 属性的只读引用,即属性懒加载
    __webpack_require__.d = (exports, definition) => {
        for(var key in definition) {
            // 判断是否实例的属性 且 未在exports中定义过
            // 目的:只使用 definition 实例的属性,为 exports 设置 getter,且不重复设置。
            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) => {
        // 支持 Symbol,也设置 Symbol.toStringTa, Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
        }
        // 设置 exports 的 __esModule 字段为 true,即标记为 ESM 模块
        Object.defineProperty(exports, '__esModule', { value: true });
    };
})();

3.1 webpack_require.r 标记是一个 ESM 模块

暂时不知道 r 是哪个单词的缩写。

js
/* webpack/runtime/make namespace object */
(() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
        // 支持 Symbol,也设置 Symbol.toStringTa, Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
        }
        // 设置 exports 的 __esModule 字段为 true,即标记为 ESM 模块
        Object.defineProperty(exports, '__esModule', { value: true });
    };
})();

3.2 webpack_require.d 定义 get 函数(define)

js
/* webpack/runtime/define property getters */
(() => {
    // define getter functions for harmony exports
    // 定义 get 函数,实现 ESM 属性的只读引用,即属性懒加载
    __webpack_require__.d = (exports, definition) => {
        for(var key in definition) {
            // 判断是否实例的属性 且 未在exports中定义过
            // 目的:只使用 definition 实例的属性,为 exports 设置 getter,且不重复设置。
            if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
            }
        }
    };
})();

3.3 webpack_require.o 判断是否实例的属性(OwnProperty)

js
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();

3.4 Symbol.toStringTag 设定 Object.prototype.toString 返回值

Object.prototype.toString() 方法会去读取 Symbol.toStringTag 并把它包含在自己的返回值里

js
var t = {}
Object.prototype.toString.call(t) // '[object Object]'
Object.defineProperty(t, Symbol.toStringTag, { value: 'Module' });
Object.prototype.toString.call(t) // '[object Module]'

3.5 运行时代码分析

sum.js

js
// 源码
const sum = (...args) => args.reduce((x, y) => x + y, 0)

export default sum
export const name = 'sum'

// 运行时代码
// 该模块被 module_wrapper 包裹,依旧还是 module、module.exports、require
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    // 1. 标记该模块是一个 ESM 模块
  __webpack_require__.r(__webpack_exports__);

    // 2. 导出所有的属性,即 __webpack_exports__,但通过 getter/setter 方式,可以懒加载属性
  __webpack_require__.d(__webpack_exports__, {
    "default": () => (__WEBPACK_DEFAULT_EXPORT__),
    "name": () => (/* binding */ name)
  });

    // 3. 执行代码,配置 getter/setter 的属性
  const sum = (...args) => args.reduce((x, y) => x + y, 0)

    // 即 export default
  const __WEBPACK_DEFAULT_EXPORT__ = (sum);

  const name = 'sum'
})

index.js

js
// 源码
import sum, { name } from './sum'
import * as s from './sum'

console.log(sum(3, 4))
console.log(name)
console.log(s)


// 运行时代码
(() => { // webpackBootstrap
	"use strict";
	var __webpack_modules__ = ([
    /* 0 */,
    /* 1 */((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, {
        "default": () => (__WEBPACK_DEFAULT_EXPORT__),
        "name": () => (/* binding */ name)
    });
    const sum = (...args) => args.reduce((x, y) => x + y, 0)

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

    const name = 'sum'
    })
	]);


/************************************************************************/
  // 缓存都和 CJS 的运行时代码一致
	// The module cache
	var __webpack_module_cache__ = {};
	
	// The require function
  // require 与 CJS 的运行时一致
	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
    // 执行的传参和 CJS 的运行时代码一致,依旧还是 module、module.exports、require
    // 执行的传参和 `CJS` 的运行时代码一致,依旧还是 module、module.exports、require。只不过 `CJS` 是为 `module.exports` 添加数据属性或直接整个改写赋值。而 `ESM` 是为其添加访问器属性 getter。
		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
	
		// Return the exports of the module
		return module.exports; // 还是返回模块的 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__);
var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); // 得到模块的 module.exports

console.log((0,_sum__WEBPACK_IMPORTED_MODULE_0__["default"])(3, 4)) // 取得模块的访问器属性 default
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__.name) // 取得模块的访问器属性 name
console.log(_sum__WEBPACK_IMPORTED_MODULE_0__)

})();

/******/ })()
;
  1. __webpack_require__CJS 的运行时一致,意味着执行的传参和 CJS 的运行时代码一致,依旧还是 module、module.exports、require。且模块返回还是 module.exports
  2. 执行的传参和 CJS 的运行时代码一致,只不过 CJS 是为 module.exports 添加数据属性或直接整个改写赋值。而 ESM 是为其添加访问器属性 getter。

疑问

  • [ ]