灏天阁

webpack5之模块化原理

· Yin灏

模块化原理

在 webpack5 中,我们既能使用  CommonJS,又能使用ES Module,还能支持其他如AMDCMDUMD等模块化规则。那 webpack 怎么实现这些模块化呢。我们讲下目前主流的CommonJSES Module这两种模块化原理,我们可以分为四种导出引入方式并查找对应的实现机制。

  • CommonJS模块化实现原理
  • ES Module实现原理
  • CommonJS加载ES Module的原理
  • ES Module加载CommonJS的原理

目前我们有两种导出文件  dateFormat.js  通过CommonJS  导出,priceFormat.js  通过ES Module 导出。

// dateFormat.js
const dateNow = () => {
  const date = new Date();
  return date.toLocaleString();
};
module.exports = {
  dateNow,
};
// priceFormat.js
const add = (a, b) => {
  return a + b;
};
const minus = (a, b) => {
  return a - b;
};
export { add, minus };

CommonJS 模块化实现原理

我们在入口文件 main.js 引入 dateFormat.js。

// main.js
const { dateNow } = require("./js/dateFormat");
console.log(dateNow());

因为我们还需要能够查看 webpack 打包源码,因此还需要做以下配置

module.exports = {
  //...
  mode: "development",
  devtool: "source-map",
  //...
};

重新打包后我们可以查看 bundle.js 源码,并将一些无效注释去掉。

var __webpack_modules__ = {
  "./src/js/dateFormat.js": function (module) {
    const dateNow = () => {
      const date = new Date();
      return date.toLocaleString();
    };
    module.exports = {
      dateNow,
    };
  },
};

// The module cache
var __webpack_module_cache__ = {};

// The require function
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
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  // Return the exports of the module
  return module.exports;
}

var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!(function () {
  const { dateNow } = __webpack_require__(
    /*! ./js/dateFormat */ "./src/js/dateFormat.js"
  );
  console.log(dateNow());
})();
//# sourceMappingURL=bundle.js.map

去掉注释后的源码已经很简单了,可以从中得知首先会定义所有模块  __webpack_modules__ ,其中里面 key 为模块路径,value 为模块函数,里面存放了相应的执行函数。第二步会定义一个缓存空对象  __webpack_module_cache__ 。第三步会定义一个引用模块方法  __webpack_require__ ,最后一步才是真正执行我们要执行的方法,可以看到  const { dateNow } = __webpack_require__(/*! ./js/dateFormat */ "./src/js/dateFormat.js") ,调用了引用方法并将模块路径传入进去,在  __webpack_require__  这个引用方法中会先对缓存对象  __webpack_module_cache__  做判断,如果存在缓存则返回  cachedModule.exports ,否则就将 key 值: 模块路径和 value 值: { exports: {} }  放进缓存对象中,并赋值给  var module ,之后再调用模块对象  __webpack_modules__  取对应的模块,将  module  作为参数传进去,并返回  module.exports = { dateNow } 。最后返回  module.exports ,__webpack_require__  返回并将  dateNow  解构出来打印。

做个小结:

  1. 生成模块对象  __webpack_modules__
  2. 申明模块缓存对象__webpack_module_cache__
  3. 申明模块引用函数__webpack_require__
  4. 执行模块引用函数从  __webpack_modules__  模块对象里面返回方法并执行后返回属性方法对象

ES Module 模块化实现原理

现在改造 main.js 引入 priceFormat.js。

// main.js
import { add, minus } from './js/priceFormat'
console.log(add(2, 3))
console.log(minus(5, 3))

重新打包并去掉一些干扰的注视后可以看到

var __webpack_modules__ = {
  "./src/js/priceFormat.js": function (
    __unused_webpack_module,
    __webpack_exports__,
    __webpack_require__
  ) {
    __webpack_require__.r(__webpack_exports__);
    __webpack_require__.d(__webpack_exports__, {
      add: function () {
        return /* binding */ add;
      },
      minus: function () {
        return /* binding */ minus;
      },
    });
    const add = (a, b) => {
      return a + b;
    };
    const minus = (a, b) => {
      return a - b;
    };
  },
};
// The module cachevar
__webpack_module_cache__ = {};
// The require function
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
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  // Return the exports of the module
  return module.exports;
}
/* webpack/runtime/define property getters */
!(function () {
  // define getter functions for harmony exports
  __webpack_require__.d = function (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 */
!(function () {
  __webpack_require__.o = function (obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
  };
})();
/* webpack/runtime/make namespace object */
!(function () {
  // define __esModule on exports
  __webpack_require__.r = function (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.
!(function () {
  /*!*********************!*\    !*** ./src/main.js ***!    *********************/
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */
  var _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
    /*! ./js/priceFormat */ "./src/js/priceFormat.js"
  );
  console.log((0, _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__.add)(2, 3));
  console.log((0, _js_priceFormat__WEBPACK_IMPORTED_MODULE_0__.minus)(5, 3));
})();
//# sourceMappingURL=bundle.js.map

ES Module 模块话显然比 CommonJS 代码多了很对,我们来看下,首先也是先定义一个模块对象  __webpack_modules__ ,里面存放着  ./src/js/priceFormat.js  模块,第二步,定义一个  __webpack_module_cache__  缓存对象,第三步定义引用模块函数  __webpack_require__ 。可以看到第二步和第三步和  CommonJS  定义的一摸一样,之后又定义了几个函数  __webpack_require__.d ,__webpack_require__.o ,__webpack_require__.r ,这些函数之后执行到了再说,之后定义了__webpack_exports__  为空对象。在最后一个执行函数里面才开始真正执行我们需要用的代码。可以看到先调用__webpack_require__.r(__webpack_exports__)  方法。__webpack_require__.r  其实为传入的exports  打上  __esModule  属性,用来表示是 ES Module 模块,因为我们入口是 main.js 用 ES Module 模块化引入。最后执行引用方法  __webpack_require__ ,传入模块路径  ./src/js/priceFormat.js ,通过之前的 CommonJS 介绍,引用方法里取的就是  __webpack_modules__  对应模块路径的函数并调用。在模块函数里面首先也是先调用了  __webpack_require__.r  方法,给导入的  module.exports  标识为 ES Module 模块,然后调用  __webpack_require__.d  方法,可以看到我们除了传入  module.exports,还传入了definition: { "add": function () { return add; }, "minus": function () { return minus; }});这个对象,可以看到这是一个导出的方法名为 key,value 为一个返回该导出方法的函数。我们看下这个  __webpack_require__.d  干了什么呢,首先遍历  definition  的 key,调用  __webpack_require__.o  方法判断 key 是否是传入该对象自身的属性,且在传入的module.exports  上没有该属性。如果条件满足,则对  module.exports  做一层代理,Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }) ,当我们访问  module.exports  上的属性方法时,它会从  definition  对象上取。

小结:

  1. 生成模块对象  __webpack_modules__
  2. 申明模块缓存对象__webpack_module_cache__
  3. 申明模块引用函数__webpack_require__
  4. 申明  __webpack_require__.d ,__webpack_require__.o ,__webpack_require__.r
  5. 执行模块引用函数从  __webpack_modules__  模块对象里面返回方法后执行
  6. __webpack_modules__  取得的方法里面首先调用  __webpack_require__.r  定义模块__esModule  属性,代表是 ES Module 模块。
  7. 之后定义一个由属性方法为 key,value 为函数且返回该属性方法的对象  definition  和  module.exports  一起传入  __webpack_require__.d  方法里面
  8. __webpack_require__.d  方法里面首先会对  definition  和  module.exports  通过__webpack_require__.o  方法判断属性的归属
  9. 条件满足后执行  Object.defineProperty(exports, key, { enumerable: true, get: definition[key] })  对  module.exports  做一层代理,当我们访问  module.exports  上的属性方法时,它会从  definition  对象上取。

剩下的CommonJS  加载ES Module  和  ES Module  加载CommonJS  的原理其实跟上述一样,对于 CommonJS 会直接从  __webpack_modules__  取,对于  ES Module  会对属性方法做一层代理,我们在访问  ES Module  引入的模块时,会代理到代理对象上面去取。

- Book Lists -