webpack5之模块化原理

800 阅读6分钟

webpack系列目录

  1. webpack5之核心配置梳理
  2. webpack5之模块化原理
  3. webpack5之Babel/ESlint/浏览器兼容
  4. webpack5之性能优化
  5. webpack5之Loader和Plugin的实现
  6. webpack5之核心源码解析

模块化原理

在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 引入的模块时,会代理到代理对象上面去取。