webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。
无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。
CommonJS 规范
假设现在有如下两个文件:
// index.js const test2 = require('./test2') function test() {} test() test2()
// test2.js function test2() {} module.exports = test2
以上两个文件使用 CommonJS 规范来导入导出文件,打包后的代码如下(已经删除了不必要的注释):
(function(modules) { // webpackBootstrap // The module cache // 模块缓存对象 var installedModules = {}; // The require function // webpack 实现的 require() 函数 function __webpack_require__(moduleId) { // Check if module is in cache // 如果模块已经加载过,直接返回缓存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) // 创建一个新模块,并放入缓存 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function // 执行模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded // 将模块标识为已加载 module.l = true; // Return the exports of the module return module.exports; } // expose the modules object (__webpack_modules__) // 将所有的模块挂载到 require() 函数上 __webpack_require__.m = modules; // expose the module cache // 将缓存对象挂载到 require() 函数上 __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; // 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 }); }; // create a fake namespace object // mode & 1: value is a module id, require it // mode & 2: merge all properties of value into the ns // mode & 4: return value when already ns object // mode & 8|1: behave like require __webpack_require__.t = function(value, mode) { if(mode & 1) value = __webpack_require__(value); if(mode & 8) return value; if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value: value }); if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // Load entry module and return exports // 加载入口模块,并返回模块对象 return __webpack_require__(__webpack_require__.s = "./src/index.js"); })({ "./src/index.js": (function(module, exports, __webpack_require__) { eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?"); }), "./src/test2.js": (function(module, exports) { eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?"); }) });
可以看到 webpack 实现的模块加载系统非常简单,仅仅只有一百行代码。
打包后的代码其实是一个立即执行函数,传入的参数是一个对象。这个对象以文件路径为 key,以文件内容为 value,它包含了所有打包后的模块。
{ "./src/index.js": (function(module, exports, __webpack_require__) { eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?"); }), "./src/test2.js": (function(module, exports) { eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?"); }) }
将这个立即函数化简一下,相当于:
(function(modules){ // ... })({ path1: function1, path2: function2 })
再看一下这个立即函数做了什么:
- 定义了一个模块缓存对象
installedModules
,作用是缓存已经加载过的模块。 - 定义了一个模块加载函数
__webpack_require__()
。 - ... 省略一些其他代码。
- 使用
__webpack_require__()
加载入口模块。
其中的核心就是 __webpack_require__()
函数,它接收的参数是 moduleId
,其实就是文件路径。
它的执行过程如下:
- 判断模块是否有缓存,如果有则返回缓存模块的
export
对象,即module.exports
。 - 新建一个模块
module
,并放入缓存。 - 执行文件路径对应的模块函数。
- 将这个新建的模块标识为已加载。
- 执行完模块后,返回该模块的
exports
对象。
// The require function // webpack 实现的 require() 函数 function __webpack_require__(moduleId) { // Check if module is in cache // 如果模块已经加载过,直接返回缓存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) // 创建一个新模块,并放入缓存 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function // 执行模块函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded // 将模块标识为已加载 module.l = true; // Return the exports of the module return module.exports; }
从上述代码可以看到,在执行模块函数时传入了三个参数,分别为 module
、module.exports
、__webpack_require__
。
其中 module
、module.exports
的作用和 CommonJS 中的 module
、module.exports
的作用是一样的,而 __webpack_require__
相当于 CommonJS 中的 require
。
在立即函数的最后,使用了 __webpack_require__()
加载入口模块。并传入了入口模块的路径 ./src/index.js
。
__webpack_require__(__webpack_require__.s = "./src/index.js");
我们再来分析一下入口模块的内容。
(function(module, exports, __webpack_require__) { eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?"); })
入口模块函数的参数正好是刚才所说的那三个参数,而 eval 函数的内容美化一下后和下面内容一样:
const test2 = __webpack_require__("./src/test2.js") function test() {} test() test2() //# sourceURL=webpack:///./src/index.js?
将打包后的模块代码和原模块的代码进行对比,可以发现仅有一个地方发生了变化,那就是 require
变成了 __webpack_require__
。
再看一下 test2.js
的代码:
function test2() {} module.exports = test2 //# sourceURL=webpack:///./src/test2.js?
从刚才的分析可知,__webpack_require__()
加载模块后,会先执行模块对应的函数,然后返回该模块的 exports
对象。而 test2.js
的导出对象 module.exports
就是 test2()
函数。所以入口模块能通过 __webpack_require__()
引入 test2()
函数并执行。
到目前为止可以发现 webpack 自定义的模块规范完美适配 CommonJS 规范。
ES6 module
将刚才用 CommonJS 规范编写的两个文件换成用 ES6 module 规范来写,再执行打包。
// index.js import test2 from './test2' function test() {} test() test2()
// test2.js export default function test2() {}
使用 ES6 module 规范打包后的代码和使用 CommonJS 规范打包后的代码绝大部分都是一样的。
一样的地方是指 webpack 自定义模块规范的代码一样,唯一不同的是上面两个文件打包后的代码不同。
{ "./src/index.js":(function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?"); }), "./src/test2.js": (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?"); }) }
可以看到传入的第二个参数是 __webpack_exports__
,而 CommonJS 规范对应的第二个参数是 exports
。将这两个模块代码的内容美化一下:
// index.js __webpack_require__.r(__webpack_exports__); var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js"); function test() {} test() Object(_test2__WEBPACK_IMPORTED_MODULE_0__["default"])() //# sourceURL=webpack:///./src/index.js? // test2.js __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "default", function() { return test2; }); function test2() {} //# sourceURL=webpack:///./src/test2.js?
可以发现,在每个模块的开头都执行了一个 __webpack_require__.r(__webpack_exports__)
语句。并且 test2.js
还多了一个 __webpack_require__.d()
函数。
我们先来看看 __webpack_require__.r()
和 __webpack_require__.d()
是什么。