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


    这篇文章不涉及 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,
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    npm 安装三个 node 包。

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

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

    小试牛刀

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

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

    打包产物:

    (() => {
        var __webpack_exports__ = {};
        document.write("hello, liang");
    })(); 
    
    • 1
    • 2
    • 3
    • 4

    只是简单的包了层 IIFE

    commonjs 模块

    写一个 add 模块函数

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

    然后 index.js 进行调用。

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

    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;
      },
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    function __webpack_require__(moduleId) {
      var module  = {
        exports: {},
      });
    
      __webpack_modules__[moduleId](
        module,
        module.exports,
        __webpack_require__
      );
    
      return module.exports;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    因为 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;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    然后看下整体代码,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));
        })();
    })(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    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; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后是 index.js

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

    此时运行一下会发现和 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;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    第一个是 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],
          });
        }
      }
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

    第三个是 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,
      });
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这几个方法啥时候用呢,会在我们的模块代码之前调用。

    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;
            },
        }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们把 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)
            );
        })(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到我们是通过 _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)
            );
        })();
    })(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    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;
            },
        }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    一个最大的区别就是 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)); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果只看 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],
          });
        }
      }
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

    image-20220503115321218

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

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

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

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

  • 相关阅读:
    Spring Boot应用部署 - Tomcat容器替换为Undertow容器
    【无标题】
    C# 连接SQL Sever 数据库与数据查询实例 数据仓库
    从助力跨境互通到保障农民工,区块链在大湾区做了什么? | 研讨会
    一文了解tcp/ip协议的运行原理
    java Spring Boot在配置文件中关闭热部署
    【PyQt小知识 - 4】:QGroupBox分组框控件 - 边框和标题设置
    【面试】卡夫卡Kafka相关
    1天精通Apipost--全网最全gRPC调试和智能Mock讲解!
    Java之方法(6)
  • 原文地址:https://blog.csdn.net/weixin_53312997/article/details/125541281