webpack 模块化原理解析

971 阅读8分钟

前言

本文将探究 CommonJsES Module两种规范的区别,并从打包后的代码对 webpack 是如何处理两种模块化方式做出解析。

CommonJs 和 ES Module

CommonJs 规范

对于 CommonJs 规范,Node 是其具有代表性的一个实现,而它具备以下特点:

  • 在 Node 中,每一个 JS 文件都被看作是一个模块
  • CommonJs 使用 module.exports、exports 导出模块,使用 require 引入模块

比如:

//a.js
const name = 'Jolyne' 
const age = 22
module.exports = { name, age }
//或者
exports.name = 'Jolyne'
exports.age = 22

//main.js
const { name, age } = require("./a.js")
console.log(name, age) // Jolyne 22

ES Module 规范

对于 ES Module 规范,它借鉴了 CommonJs,它具有以下特点:

  • ES Module 使用 export 导出模块,使用 import 引入模块
  • import() 引入模块,可以实现懒加载
  • ES Module 静态导入的方式,可以实现 tree shaking

比如:

//a.js
export const name = 'Joylne'
export default function getName() { return name }

//main.js
import getName, { name } from "./a.js"
console.log(getName(), name) // Jolyne Jolyne
//或者
import * as module from "./a.js"
console.log(module.getName(), module.name) // Jolyne Jolyne
//或者动态导入
const promise = import("./a.js")

webpack 模块化原理

在 webpack 里面使用的模块化基本上是 ES ModuleCommonJs,接下来我们来看看 webpack 是如何处理的

webpack 中使用 CommonJs

//a.js
const name = "Joylne";
module.exports = { name };

//main.js
const {name} = require("./a")
console.log(name) //Jolyne

我们直接打包,根据打包后的文件我们来分析 webpack 是如何处理 CommonJs 模块化的(在这里,我把打包后的代码的注释去掉,加上了我自己的注释

//打包后的代码
// webpack 打包后,会将编译后的结果放到这个立即执行函数中
(() => {
  //定义了一个模块导出对象,以模块的路径为 key,value 是一个函数,函数里面就是对应模块编写的源代码
  var __webpack_modules__ = {
    "./src/a.js": (module) => {
      const name = "Joylne";
      module.exports = { name };
    },
  };
  
  //定义了一个模块缓存对象
  var __webpack_module_cache__ = {};

  //定义了一个加载模块的函数,这里的参数 moduleId 其实就是 某个模块的路径,比如下面调用的 './src/a.js'
  function __webpack_require__(moduleId) {
    // 1、判断当前模块有没有缓存
    var cachedModule = __webpack_module_cache__[moduleId];
    // 2、如果缓存了,直接返回缓存的内容
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }

    // 3、如果没有缓存当前模块,那就创建一个 { exports: {} } 对象,把这个对象的地址 赋值给 module 变量 以及 缓存对象
    // 也就是说: var module = cache = { exports: {} }, module 和 cache 的值是一样的,存的都是 { exports: {} } 这个对象的地址
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });

    // 4、从模块导出对象里面,找到对应模块,然后执行对应的函数,然后就会得到:{ exports: { 模块里面的数据 }} 
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // 5、最后,将 { exports: { 模块中的数据 }} 返回回去
    return module.exports;
  }

  var __webpack_exports__ = {};

  (() => {
    // 这里调用 require 函数,就能拿到模块导出的值了
    const { name } = __webpack_require__("./src/a.js");
    console.log(name);
  })();
})();

从打包后的代码分析,我们可以知道:

  1. 首先,webpack 打包后,会将打包的内容放入一个立即执行函数中,目的是防止命名冲突

  2. 定义了一个 __webpack_modules__ 模块对象,这个对象以 JS 模块的路径作为 key函数作为 value函数体的内容是模块的源代码

  3. 定义了一个 __webpack_module_cache__缓存对象,用于缓存模块内容(比如对 webpack 开启缓存后,打包构建的速度会变快)

  4. 定义了一个 __webpack_require__的执行函数(重点),以模块路径作为参数

    1. 判断当前模块有没有缓存(也就是从 __webpack_module_cache__ 中看拿不拿得到缓存数据)
    2. 如果缓存了,直接返回缓存的内容
    3. 如果没有缓存,则会创建一个 { exports: {} } 对象,这个对象其实就是后面处理过后 return 的结果,然后在定义一个 module 变量,将 { exports: {} } 的地址赋值给 module__webpack_module_cache__[模块路径](这样就既能将最终的结果返回出去,也可以缓存结果)
    4. 然后通过模块路径,通过 __webpack_modules__[模块路径] 拿到对应的函数,执行函数,最后将 { exports: { 模块数据 } } 返回
  5. 调用 __webpack_require__ 函数,得到结果,最终打印出来

从上面的流程中可以知道:

webpack 对于 CommonJs 的处理,实际上就是将模块里面的数据,最终给了 module.exports 这个对象,然后将 module.exports 这个对象返回,我们就可以通过结构拿到里面的 name 了

图解如下:

image.png

webpack 中使用 ES Module

// a.js
export const name = "Joylne";
const age = 23;
export default age;  

//main.js
import age, { name } from "./a.js";
console.log(age, name); // 23  Joylne

打包后,得到的结果如下:

(() => {
  "use strict";

  //1、定义模块变量 __webpack_modules__,同样以模块路径为 key,值是函数,函数体内部对模块源码进行了代理
  var __webpack_modules__ = {
    "./src/a.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      // 2、这个方法其实就是标识 当前模块 .src/a.js 是一个 ES Module 模块
      __webpack_require__.r(__webpack_exports__);
      // 3、这个方法其实就是对传进来的 { exports: {} } 这个对象进行了代理,即: { exports: { naem: () => name,  default: () => __WEBPACK_DEFAULT_EXPORT__ }}
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__,
        name: () => name,
      });

      //4、这里是将 export default 默认导出的数据,挂载到了 __WEBPACK_DEFAULT_EXPORT__ 这个变量而已
      const name = "Joylne";
      const age = 23;
      const __WEBPACK_DEFAULT_EXPORT__ = age;
    },
  };

  //5、同样的,定义一个缓存对象
  var __webpack_module_cache__ = {};

  //6、这里和 commonJS 一样
  function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    //7、先判断模块有没有缓存,有就直接 return
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }

    //8、否则就将 { exports: {} } 的地址赋值给 module、__webpack_module_cache__[moduleId]
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });

    //9、根据模块路径,执行对应的函数,得到结果
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    //10、return module.exports,在这里也就是: { naem: () => name,  default: () => __WEBPACK_DEFAULT_EXPORT__ }
    return module.exports;
  }

  (() => {
    // 这个方法就是对 exports 对象做代理,通过 Object.defineProperty 往 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],
          });
        }
      }
    };
  })();

  (() => {
    // 这个方法就是判断 obj 对象本身身上有没有某个 prop
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  (() => {
    // 这个方法就是标识当前模块是 ES Module 模块,通过 Symbol.toStringTag 自定义了类型
    __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 _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");

    console.log(
      _a_js__WEBPACK_IMPORTED_MODULE_0__["default"], // 23
      _a_js__WEBPACK_IMPORTED_MODULE_0__.name // Jolyne
    );
  })();
})();

其实 webpack 对于 ES Module 的处理和对 CommonJS 的处理大同小异,区别在于:

  • 处理 CommonJS 时,因为导出语法是 module.exports = XXX 或 exports = XXX,所以是通过 require 函数将模块的数据赋值给了 exports 对象再返回
  • 处理 ES Module 时,因为导出语法是 export 或 export default,并没有 exports 对象给我们赋值,所以 require 函数对我们创建的 { exports: {} } 对象进行了代理,将模块的数据通过 { name: () => name } 的形式,挂载到 exports 对象身上再返回

图解如下:

image.png

注意:当访问 export default 属性时,其实访问的是 DEFAULT_EXPORT,访问 export const 属性时,其实访问的是 () => xxx

webpack 中使用 CommonJS 加载 ES Module

//a.js
export const name = "Joylne";
const age = 23;
export default age;  

//main.js
const obj = require('./a');
console.log(obj); // { default: [Getter], name: [Getter]}

打包后的代码如下:

(() => {
  var __webpack_modules__ = {
    "./src/a.js": (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      "use strict";
      // 同样,对 exports 对象做了代理
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(__webpack_exports__, {
        default: () => __WEBPACK_DEFAULT_EXPORT__,
        name: () => name,
      });
      const name = "Joylne";
      const age = 23;
      const __WEBPACK_DEFAULT_EXPORT__ = age;
    },
  };

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

  (() => {
    const obj = __webpack_require__("./src/a.js");
    console.log(obj);
  })();
})();

其实当 CommonJS 引用 ES Module 导出的模块时,和 webpack 处理 ES Module 的情况基本相同,都是对 { exports: {} } 做了代理,将模块数据挂载到 exports 对象上然后返回。唯一的区别就是:

(() => {
    // 也就是引用的时候,这里和处理 CommonJS 是一样的
    const obj = __webpack_require__("./src/a.js");
    console.log(obj);
})();

webpack 中使用 ES Module 加载 CommonJS

(() => {
  var __webpack_modules__ = {
    // 同样,这里是直接赋值给 exports 对象,而不会去做代理
    "./src/a.js": (module) => {
      const name = "Jolyne";
      module.exports = name;
    },
  };

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

  (() => {
    // 这个函数是用来返回 默认导出的内容,最终会挂载到 exports 对象上,key 是 "default"
    __webpack_require__.n = (module) => {
      var getter =
        module && module.__esModule ? () => module["default"] : () => module;
      __webpack_require__.d(getter, { a: getter });
      return getter;
    };
  })();

  (() => {
    // 这个方法是通过 Object.defineProperty 对 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);
  })();

  (() => {
    // 标识当前模块是 ES Module 导出的模块
    __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__ = {};
  (() => {
    "use strict";

    __webpack_require__.r(__webpack_exports__);
    var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
    var _a__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
      _a__WEBPACK_IMPORTED_MODULE_0__
    );

    console.log(_a__WEBPACK_IMPORTED_MODULE_0__.name);
  })();
})();

这种情况就多了一个 __webpack_require__.n 函数,他做的事情就是把默认导出的内容挂载到 exports对象上,核心就是把模块数据赋值给 exports 对象

总结

  • 当模块通过 CommonJS 的方式导出时,会直接将模块的数据赋值给 exports 对象然后返回

  • 当模块通过 ES Module 的方式导出时,会创建一个 { exports: {} }对象,然后通过 Object.defineProperty 的方式为 exports 对象做个代理,以 { key: () => data } 的形式赋值给 exports 对象然后导出

  • 当模块通过 CommonJS 导出、ES Module 导入时,同样直接将模块数据赋值给 exports 对象并返回,再通过 __webpack_require__.n 函数将默认导出的部分再赋值给 exports 对象

  • 当模块通过 ES Module 导出、CommonJS 导入时,也是会创建 { exports: {} }对象,通过 Object.defineProperty 的方式为 exports 对象做个代理,以 { key: () => data } 的形式赋值给 exports 对象然后导出

再简单暴力点总结就是:

  • CommonJS 方式导出模块时,直接将模块数据赋值个 exports 对象并返回
  • ES Module 方式导出模块时,创建 exports 对象并对其做代理,挂载导出的数据(正常导出和默认导出)并返回

结语

以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。