webpack中的模块化实现原理

358 阅读6分钟

相信对于webpack大家并不陌生,我们的项目很多都用到它,不知道大家有没有注意到,在使用webpack开发项目的时候,我们可以使用多种模块化的导入导出方式,比如CommonJS和ES module,甚至还可以混用:比如在ES module的模块使用import导入使用module.exports导出的内容。这些操作其实是webpack在给我们兜底,如果你没有使用webpack,是不能像上面说的那样混用的,那么webpack是如何做的呢?

CommonJS

我们先来研究webpack是如何处理CommonJS模块化的。
首先我们编写两个文件来测试:

// src/cmj.js
module.exports = {
  sum(a, b) {
    return a + b;
  }
}
// src/index.js
const { sum } = require('./cmj')
console.log(sum(1, 2))

上面是一个简单的CommonJS模块的导入导出。我们使用webpack对这两个文件进行打包,然后来看打包后代码,这里注意要配置devtool为source-map,以方便我们阅读。下面是webpack配置:

// webpack.config.js
module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devtool: 'source-map'
};

运行打包命令后得到打包后的代码如下(为了方便阅读已经去除了不必要的注释):

// dist/main.js
(() => {
  // 声明一个对象,用来存储模块
  // key是模块的路径,value是该模块对应的函数
  var __webpack_modules__ = {
    './src/cmj.js':
      /*!********************!*\
  !*** ./src/cmj.js ***!
  \********************/
      (module) => {
        module.exports = {
          sum(a, b) {
            return a + b;
          },
        };
      },
  };
  // The module cache
  // 缓存已经加载过的模块
  var __webpack_module_cache__ = {};
  // The require function
  // webpack的require函数
  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__ = {};
  (() => {
    /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
    const { sum } = __webpack_require__(/*! ./cmj */ './src/cmj.js');
    console.log(sum(1, 2));
  })();
})();

解析:可以看到最外面是一个立即执行函数;进入立即执行函数,首先声明了一个变量__webpack_modules__,key是模块路径,value是对应模块的内容的一个函数,我们这里只有一对key、value是因为我们只使用了这一对模块,如果有多个模块,这里会有多个key、value;接着下面又声明了一个__webpack_module_cache__用来缓存已经被加载的模块来保证一个模块只被加载一次以提升性能;下面声明了一个函数__webpack_require__,入参是一个模块路径,用来获取__webpack_modules__对应的模块函数,执行对应的模块函数并返回module.exports;接下来又是一个立即执行函数,函数内部调用了__webpack_require__,传入对应的模块路径,拿到对应模块函数执行完成后的module.exports对象,解构出sum函数然后执行。

ES module

接下来看ES module,先编写测试代码:

// src/esm.js
export const sum = (a, b) => {
  return a + b
}
export default function (a, b) {
  return a - b
}
// src/index.js
import sub, { sum } from './esm';
sub(10, 5)
sum(10, 5)

然后build出以下代码:

// dist/main.js
(() => {
  // webpackBootstrap
  'use strict';
  var __webpack_modules__ = {
    './src/esm.js': (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) => {
      // 执行r方法 给__webpack_exports__对象添加__esModule属性 值为true 表示该模块是ES模块
      __webpack_require__.r(__webpack_exports__);
      // 执行d方法 给__webpack_exports__对象添加 default和sum属性
      __webpack_require__.d(__webpack_exports__, {
        default: () => /* export default binding */ __WEBPACK_DEFAULT_EXPORT__,
        sum: () => /* binding */ sum,
      });
      const sum = (a, b) => {
        return a + b;
      };

      /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(a, b) {
        return a - b;
      }
    },
  };

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

  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony exports
    // 在__webpack_require__上绑定了一个d方法
    __webpack_require__.d = (exports, definition) => {
      // 遍历definition对象
      for (var key in definition) {
        // 如果definition对象有key属性 并且exports对象没有key属性
        // 给exports对象添加key属性 值为definition[key]
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          });
        }
      }
    };
  })();

  /* webpack/runtime/hasOwnProperty shorthand */
  (() => {
    // 在__webpack_require__上绑定了一个o方法
    // 用来判断对象obj是否有prop属性
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    // 在__webpack_require__上绑定了一个r方法
    __webpack_require__.r = (exports) => {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        // Symbol.toStringTag 内置通用(well-known)symbol 是一个字符串值属性,
        // 用于创建对象的默认字符串描述。它由 Object.prototype.toString() 方法内部访问。
        // 给exports对象添加Symbol.toStringTag属性 值为Module
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
      }
      // 给exports对象添加__esModule属性 值为true
      Object.defineProperty(exports, '__esModule', { value: true });
    };
  })();
  // 声明__webpack_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.
  (() => {
    /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
    // 执行r方法
    __webpack_require__.r(__webpack_exports__);
    var _esm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
      /*! ./esm */ './src/esm.js'
    );

    (0, _esm__WEBPACK_IMPORTED_MODULE_0__['default'])(10, 5);
    (0, _esm__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 5);
  })();
})();

解析:可以看到这个代码与CommonJS模块化的代码很相似,__webpack_modules__、__webpack_module_cache__的作用是一致的,__webpack_require__函数的实现也一样,不同的是webpack_require函数上被添加了几个方法:o方法用来判断obj本身是否存在prop属性;d方法用来遍历definition上的属性,如果exports本身没有key,则添加到exports;r方法用来给__webpack_exports__添加标记,这里暂时用不到。重点是__webpack_modules__的value与CommonJS的不同,其内部调用r方法,然后是重点的d方法,将__webpack_exports__和我们导出的ES module对象传入,给__webpack_exports__对象赋值。这样es模块的导出exports就处理完成了,后续的 var _esm__WEBPACK_IMPORTED_MODULE_0__就能拿到对应的exports了。

CommonJS引入ES module

下面我们将入口文件改成这样:

const esm = require('./esm');
const { sum } = require('./cmj');
console.log(
  esm.default(1, 2),
  esm.sum(1, 2),
  sum(1, 2)
)

打包后:

(() => {
  // webpackBootstrap
  var __webpack_modules__ = {
    './src/cmj.js':
      /*!********************!*\
  !*** ./src/cmj.js ***!
  \********************/
      (module) => {
        module.exports = {
          sum(a, b) {
            return a + b;
          },
        };
      },

    './src/esm.js':
      /*!********************!*\
  !*** ./src/esm.js ***!
  \********************/
      (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        'use strict';
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, {
          default: () =>
            /* export default binding */ __WEBPACK_DEFAULT_EXPORT__,
          sum: () => /* binding */ sum,
        });
        const sum = (a, b) => {
          return a + b;
        };

        /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(a, b) {
          return a - b;
        }
      },
  };

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

  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    __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__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
    const esm = __webpack_require__(/*! ./esm */ './src/esm.js');
    const { sum } = __webpack_require__(/*! ./cmj */ './src/cmj.js');

    console.log(esm.default(1, 2), esm.sum(1, 2), sum(1, 2));
  })();
})();

解析:可以看到内容就是CommonJS与ES module的结合体,逻辑也大致相同

ES module引入CommonJS模块

入口文件改成:

// src/index.js
import sub, { sum } from './esm';
import cmj from './cmj';

sub(10, 5)
sum(10, 5)
console.log(cmj.sum(10, 5))

打包后:

// dist/main.js
(() => {
  // webpackBootstrap
  var __webpack_modules__ = {
    './src/cmj.js':
      /*!********************!*\
  !*** ./src/cmj.js ***!
  \********************/
      (module) => {
        module.exports = {
          sum(a, b) {
            return a + b;
          },
        };
      },

    './src/esm.js':
      /*!********************!*\
  !*** ./src/esm.js ***!
  \********************/
      (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        'use strict';
        __webpack_require__.r(__webpack_exports__);
        __webpack_require__.d(__webpack_exports__, {
          default: () =>
            /* export default binding */ __WEBPACK_DEFAULT_EXPORT__,
          sum: () => /* binding */ sum,
        });
        const sum = (a, b) => {
          return a + b;
        };

        /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(a, b) {
          return a - b;
        }
      },
  };

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

  /* webpack/runtime/compat get default export */
  (() => {
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = (module) => {
      var getter =
        module && module.__esModule ? () => module['default'] : () => module;
      __webpack_require__.d(getter, { a: getter });
      return getter;
    };
  })();

  /* webpack/runtime/define property getters */
  (() => {
    // define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
  (() => {
    __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
  })();

  /* webpack/runtime/make namespace object */
  (() => {
    // define __esModule on exports
    __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__ = {};
  // This entry need to be wrapped in an IIFE because it need to be in strict mode.
  (() => {
    'use strict';
    /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
    __webpack_require__.r(__webpack_exports__);
    var _esm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
      /*! ./esm */ './src/esm.js'
    );
    var _cmj__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
      /*! ./cmj */ './src/cmj.js'
    );
    var _cmj__WEBPACK_IMPORTED_MODULE_1___default =
      /*#__PURE__*/ __webpack_require__.n(_cmj__WEBPACK_IMPORTED_MODULE_1__);

    (0, _esm__WEBPACK_IMPORTED_MODULE_0__['default'])(10, 5);
    (0, _esm__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 5);
    console.log(_cmj__WEBPACK_IMPORTED_MODULE_1___default().sum(10, 5));
  })();
})();

逻辑与CommonJS加载ES module非常类似,不同的一点是多了一个n方法,该方法通过__esModule属性区分是否是ES模块,从而生成不同的getter函数以获取对应模块的默认导出对象。