Webpack 打包commonjs 和esmodule 模块的产物对比

2,536 阅读6分钟

这篇文章不涉及 Webpack 的原理,只是观察下 Webpackcommonjsesmodule 模块打包后的产物,读完后会对模块系统有个更深的了解。

环境配置

Webpack 只配置入口和出口,并且将 devtool 设置为 false,把 sourcemap 关掉。

// webpack.config.js
const path = require("path");

module.exports = {
    entry: "./src/commonjs/index.js",
    output: {
        path: path.resolve(__dirname, "./dist"),
        filename: "main.js",
    },
    devServer: {
        static: path.resolve(__dirname, "./dist"),
    },
    devtool: false,
};

npm 安装三个 node 包。

npm i -D webpack webpack-cli webpack-dev-server

更详细的过程可以参考 2021年从零开发前端项目指南

小试牛刀

先简单写行代码测试一下:

// src/commonjs/index.js
document.write("hello, liang");

打包产物:

(() => {
    var __webpack_exports__ = {};
    document.write("hello, liang");
})();

只是简单的包了层 IIFE

commonjs 模块

写一个 add 模块函数

// src/commonjs/add.js
console.log("add开始引入");
module.exports.add = (a, b) => {
    return a + b;
};
exports.PI = 3.14;

然后 index.js 进行调用。

// src/commonjs/index.js
console.log("commonjs开始执行");
const { add } = require("./add");
document.write("1+1=", add(1, 1));

image-20220503162217512

分析一下打包产物。

变成了 key、value 的键值对,key 是文件名,value 是封装为一个函数的模块,提供 moduleexports 参数。

这里我们只有一个模块,所以只有一个 key

var __webpack_modules__ = {
  "./src/commonjs/add.js": (module, exports) => {
    console.log("add开始引入");
    module.exports.add = (a, b) => {
      return a + b;
    };
    exports.PI = 3.14;
  },
};

提供一个 __webpack_require__ 方法用来导入上边 __webpack_modules__ 中的模块。

function __webpack_require__(moduleId) {
  var module  = {
    exports: {},
  });

  __webpack_modules__[moduleId](
    module,
    module.exports,
    __webpack_require__
  );

  return module.exports;
}

因为 moduleexports 都是对象,所以在 __webpack_modules__ 中给 exports 添加值就是改变这里外边的值。

最后把 module.exports 返回即可。

此外,我们可以添加一个 __webpack_module_cache__ 变量来保存已经导出过的对象。

var __webpack_module_cache__ = {};

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;
}

然后看下整体代码,index.js 中通过 __webpack_require__ 方法导入模块即可。

(() => {
    var __webpack_modules__ = {
        "./src/commonjs/add.js": (module, exports) => {
            console.log("add开始引入");
            module.exports.add = (a, b) => {
                return a + b;
            };
            exports.PI = 3.14;
        },
    };

    var __webpack_module_cache__ = {};

    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;
    }

    var __webpack_exports__ = {};

    (() => {
        console.log("commonjs开始执行");
        const { add } = __webpack_require__("./src/commonjs/add.js");

        document.write("1+1=", add(1, 1));
    })();
})();

esmodule 模块

我们把上边的 commonjs 模块改写一下。

// src/esmodule/add.js
console.log("add开始引入");
export const add = (a, b) => {
    return a + b;
};
export const PI = 3.14;
const test = 3;
export default test;

然后是 index.js

// src/esmodule/index.js
console.log("esmodule开始执行");
import { add } from "./add";
document.write("1+1=", add(1, 1));

此时运行一下会发现和 commonjs 不同的地方,代码并没有按照我们写的顺序执行,屏幕中先输出的是 add开始引入 然后才是 esmodule开始执行

image-20220503113614453

看一下打包产物应该就可以理解为什么了。

和之前一样,会提供一个 __webpack_require__ 方法来引入模块。

var __webpack_module_cache__ = {};

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;
}

不同之处在于,额外提供了几个看起来比较奇怪的方法。

第一个是 d 方法,用来将 definition 上边的属性挂到 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],
      });
    }
  }
};

第二个是 o 方法,判断 exports 方法是否有 key 属性。

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

第三个是 r 方法,给 exports 加一个 Symbol.toStringTag 属性,这样 exports.toString 返回的就是 '[object Module]

此外,再加一个 __esModule 属性,用来标识该模块是 esmodule

__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_modules__ = {
        "./src/esmodule/add.js": (
            __unused_webpack_module,
            __webpack_exports__,
            __webpack_require__
        ) => {
            __webpack_require__.r(__webpack_exports__);// 标识该模块是 esmodule
            __webpack_require__.d(__webpack_exports__, {// 将该模块里的属性、方法挂到 __webpack_exports__ 上
                add: () => add,
                PI: () => PI,
                default: () => __WEBPACK_DEFAULT_EXPORT__,
            });
            console.log("add开始引入");
            const add = (a, b) => {
                return a + b;
            };
            const PI = 3.14;
            const test = 3;
            const __WEBPACK_DEFAULT_EXPORT__ = test;
        },
    };

我们把 add、PI、__WEBPACK_DEFAULT_EXPORT__ 属性都包了箭头函数 () => add ,因此可以先在 __webpack_require__.d 函数中使用它们, __webpack_require__.d 函数之后才去定义 add、PI、__WEBPACK_DEFAULT_EXPORT__ 这些变量的值。

然后是 index.js 的使用。

var __webpack_exports__ = {};

    (() => {
        __webpack_require__.r(__webpack_exports__);
        var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
            "./src/esmodule/add.js"
        );
        console.log("esmodule开始执行");

        document.write(
            "1+1=",
            (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1)
        );
    })();

可以看到我们是通过 _add__WEBPACK_IMPORTED_MODULE_0__ 变量把 ./src/esmodule/add.js 的所有方法都拿到,然后再使用 _add__WEBPACK_IMPORTED_MODULE_0__.add 调用具体的方法。

上边还有一个奇怪的用法 (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1) ,通过逗号表达式可以改变 this 指向,参考 Why does babel rewrite imported function call to (0, fn)(...)?,至于为什么这么用还不清楚,目前不重要先跳过了。

然后看下整体代码:

(() => {
    "use strict";
    var __webpack_modules__ = {
        "./src/esmodule/add.js": (
            __unused_webpack_module,
            __webpack_exports__,
            __webpack_require__
        ) => {
            __webpack_require__.r(__webpack_exports__);
            __webpack_require__.d(__webpack_exports__, {
                add: () => add,
                PI: () => PI,
                default: () => __WEBPACK_DEFAULT_EXPORT__,
            });
            console.log("add开始引入");
            const add = (a, b) => {
                return a + b;
            };
            const PI = 3.14;
            const test = 3;
            const __WEBPACK_DEFAULT_EXPORT__ = test;
        },
    };

    var __webpack_module_cache__ = {};

    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__.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_require__.o = (obj, prop) =>
            Object.prototype.hasOwnProperty.call(obj, prop);
    })();

    (() => {
        __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__ = {};

    (() => {
        __webpack_require__.r(__webpack_exports__);
        var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
            "./src/esmodule/add.js"
        );
        console.log("commonjs开始执行");

        document.write(
            "1+1=",
            (0, _add__WEBPACK_IMPORTED_MODULE_0__.add)(1, 1)
        );
    })();
})();

commonjs 和 esmodule 的不同

两个的打包产物对比:

// commonjs
var __webpack_modules__ = {
        "./src/commonjs/add.js": (module, exports) => {
            console.log("add开始引入");
            module.exports.add = (a, b) => {
                return a + b;
            };
            exports.PI = 3.14;
        },
    };

//esmodule
var __webpack_modules__ = {
        "./src/esmodule/add.js": (
            __unused_webpack_module,
            __webpack_exports__,
            __webpack_require__
        ) => {
            __webpack_require__.r(__webpack_exports__);// 标识该模块是 esmodule
            __webpack_require__.d(__webpack_exports__, {// 将该模块里的属性、方法挂到 __webpack_exports__ 上
                add: () => add,
                PI: () => PI,
                default: () => __WEBPACK_DEFAULT_EXPORT__,
            });
            console.log("add开始引入");
            const add = (a, b) => {
                return a + b;
            };
            const PI = 3.14;
            const test = 3;
            const __WEBPACK_DEFAULT_EXPORT__ = test;
        },
    };

一个最大的区别就是 commonjs 导出的就是普通的值,一旦导入就不会改变了。而 esmodule 导出的值通过函数包装了一层,因此是动态的,导入之后再次使用可能会变化。

举个例子,对于 esmodule

// src/esmodule/add.js
console.log("add开始引入");
export let PI = 3.14;

export const add = (a, b) => {
    PI = 6;
    return a + b;
};
const test = 3;
export default test;

// src/esmodule/index.js
console.log("esmodule开始执行");
import { add, PI } from "./add";
console.log(PI, "1+1=", add(1, 1));
console.log(PI, "1+1=", add(1, 1));

如果只看 src/esmodule/index.js 的代码,我们并没有改变 PI 的值,但执行会发现 add 函数执行后 PI 的值就发生了改变:

image-20220503114207675

对于原始值, commonjs 就做不到上边的事情了,一般情况下也不要这样搞,以防出现未知 bug

此外,esmodule 在挂载属性的时候只定义了 get

__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],
      });
    }
  }
};

所以我们如果在 esmodule 模块中的去修改导入的值,会直接抛错。

console.log("esmodule开始执行");
import { add, PI } from "./add";
PI = 3;
console.log(PI, "1+1=", add(1, 1));

image-20220503115321218

commonjs 中就无所谓了,但同样也不要这样搞,以防出现未知 bug

简单对比了下 commonjsesmodule 模块的产物,其中 commonjs 比较简单,就是普通的导出对象和解构对象。但对于 esmodule 的话,导出的每一个属性会映射到一个函数,因此值是可以动态改变的。

此外 require 会按我们代码中的顺序执行,但 import 会被提升到代码最前边首先执行。

还会继续对比一下两者的动态导入、混合导入,本来想一篇文章总结完的,但有点长了,那就下篇继续吧,哈哈。

Webpack 打包 commonjs 和 esmodule 混用模块的产物对比

Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比