Webpack 打包CJS 与 ESM 产物分析

2,419 阅读3分钟

尽管现在主流浏览器已支持ESM,但是webpack仍然会将ESM转化为CommonJS。我们来看下 webpack 是 如何将ESM转化为CommonJS的。

CJS

再看ESM打包产物之前,我们先看下CJS的打包产物。

CJS 示例代码

入口文件index.jssum.js内容如下👇

// index.js
const sum = require('./sum')

console.log(sum(1, 2))


// sum.js
module.exports = (...args) => args.reduce((x, y) => x + y, 0)

webpack打包文件build.js内容如下👇

const webpack = require('webpack')

function f1() {
    return webpack({
        entry: './index.js',
        mode: 'none',
    })
}

f1().run((err, stat) => {
    if (err) throw err
    console.log('🚀 构建完成')
})

运行 node 命令

node index.js

CJS 产物分析

打包完成以后,目录下成功输出dist/main.js,内容如下👇

(() => { // webpackBootstrap
    // 模块存放在这个数组中
    var __webpack_modules__ = ([
        /* 0 */, // 相当于是index.js
        ((module) => { // 模块1 存放的就是 sum 模块
            module.exports = (...args) => args.reduce((x, y) => x + y, 0)
        })
    ]);

    // 模块缓存对象
    var __webpack_module_cache__ = {};

    // 模块加载函数
    function __webpack_require__(moduleId /* 模块id */) {
        // 检查模块是否已经在缓存中
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
            return cachedModule.exports;
        }
        // 不在缓存中则创建一个模块对象,并将其放入到缓存中
        var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        };

        // 执行这个要加载到模块函数
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

        // 返回导出的 exports 对象(当然不一定就是个对象,如果模块中将 module.exports = 1,那么就是返回 1)
        return module.exports;
    }

    var __webpack_exports__ = {};
    (() => {
        // 通过 __webpack_require__ 函数加载 模块 1(sum模块)
        const sum = __webpack_require__(1)

        console.log(sum(1, 2))
    })();

})()

解析打包后到内容如下👇:

  1. 模块函数数组 webpack_modules:用于存放模块内容的数组,其数组下标表示对应的模块函数。
  2. 模块缓存对象 webpack_module_cache:通过模块函数数组的下标缓存模块的对象。
  3. 模块加载函数 webpack_require:该函数接收一个模块id(对应的就是模块函数数组的下标),如果该模块在缓存中存在则直接返回module.exports。否则将要加载的模块缓存到模块缓存对象 webpack_module_cache 中,并返回module.exports

最后调用 webpack_require(moduleId) 就可以拿到module.exports的内容了,我们这个地方就是sum函数了

ESM

ESM示例代码

入口文件index.jssum.js内容如下👇

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

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


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

export default sum

export const name = 'sum'

webpack打包文件build.js内容如下👇

const webpack = require('webpack')

function fn() {
  return webpack({
    entry: './index.js',
    mode: 'none'
  })
}

fn().run((err, stat) => {
  if (err) throw err
  console.log('🚀 构建完成')
})

ESM 产物分析

执行node build.js打包ESM得到的产物相比较于CJS而言,在__webpack_require__上多了如下几个属性👇

  1. __webpack_require__.d方法在模块加载时通过Object.definePropertygetter定义exports导出的值。
  2. __webpack_require__.oObject.prototype.hasOwnProperty的缩写。
  3. __webpack_require__.r用来标记是ESM。
(() => {
  // 使用 getter 用以定义 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] });
      }
    }
  };
})();

(() => {
  // Object.prototype.hasOwnProperty 的简写
  __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();

(() => {
  // exports.__esModule = true,用以标记一个 ESM 模块
  __webpack_require__.r = (exports) => {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
})();

我们写的的ESM代码被打包出来的内容(包含__webpack_require__)如下👇

(() => { // webpackBootstrap
    "use strict";
    var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
            // 标记是 ESM
            __webpack_require__.r(__webpack_exports__);
            // 设置模块 exports 值
            __webpack_require__.d(__webpack_exports__, {
                // 注意这里的值是个函数,会跟 __webpack_require__.d 方法中的 Object.defineProperty 的 getter 配合使用
                "default": () => (__WEBPACK_DEFAULT_EXPORT__),
                "name": () => (/* binding */ name)
            });
            const sum = (...args) => args.reduce((x, y) => x + y, 0)

            // 常量__WEBPACK_DEFAULT_EXPORT__标记默认导出 export default
            const __WEBPACK_DEFAULT_EXPORT__ = (sum);

            const name = 'sum'
        })
    ]);

    // 模块缓存
    var __webpack_module_cache__ = {};

    //  require
    function __webpack_require__(moduleId) {
        // 检查模块是否在缓存
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
            return cachedModule.exports;
        }
        // 创建一个新模块,并将其放入到缓存中
        var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        };

        // 执行模块函数
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

        // 返回模块
        return module.exports;
    }

    (() => {
        __webpack_require__.r(__webpack_exports__);
        // 加载模块,并返回模块内容
        var _sum__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

        console.log((0, _sum__WEBPACK_IMPORTED_MODULE_0__["default"])(3, 4))
        console.log(_sum__WEBPACK_IMPORTED_MODULE_0__.name)
        console.log(_sum__WEBPACK_IMPORTED_MODULE_0__)
    })();

})();

可以看到,在 ESM 中有两种导出方式,所以webpack在将ESM转化为CJS时会将export default转化为module.exports.default,将export name这种方式转化为module.exports的属性即可。

在 CJS 中本质上只有一种导出方式——导出的结果都在module.exports上。 对 CJS 与 ESM 不了解的,可以看这篇👉 CJS与ESM