本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析。
一 说明
本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理。
本文使用的Webpack版本是4.32.2版本。
注意:之前也分析过Webpack3.10.0版本构建出来的bundle.js,通过和这次的Webpack 4.32.2版本对比,核心的构建原理基本一致,只是将模块索引id改为文件路径和名字、模块代码改为了eval(moduleString)执行的方式等一些优化改造。
二 示例
1)Webpack.config.js文件内容:
1 const path = require('path'); 2 3 module.exports = { 4 entry: './src/index.js', 5 output: { 6 filename: 'bundle.js', 7 path: path.resolve(__dirname, 'dist') 8 }, 9 mode: 'development' // 'production' 用于配置开发还是发布模式 10 };
2)创建src文件夹,添加入口文件index.js:
1 import moduleLog from './module.js'; 2 3 document.write('index.js loaded.'); 4 5 moduleLog();
3)在src目录下创建module.js文件:
1 export default function () { 2 document.write('module.js loaded.'); 3 }
4)package.json文件内容:
1 { 2 "name": "webpack-demo", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo "Error: no test specified" && exit 1", 8 "webpack": "webpack" 9 }, 10 "keywords": [], 11 "author": "", 12 "license": "ISC", 13 "devDependencies": { 14 "webpack": "^4.32.2", 15 "webpack-cli": "^3.3.2" 16 }, 17 "dependencies": { 18 "lodash": "^4.17.4" 19 } 20 }
三 执行构建
执行构建命令:npm run webpack
在dist目录下生成的bundle.js源码如下(下边代码是将注释去掉、压缩的代码还原后的代码):
1 (function (modules) { 2 // The module cache 3 var installedModules = {}; 4 // The require function 5 function __webpack_require__(moduleId) { 6 // Check if module is in cache 7 if (installedModules[moduleId]) { 8 return installedModules[moduleId].exports; 9 } 10 // Create a new module (and put it into the cache) 11 var module = installedModules[moduleId] = { 12 i: moduleId, 13 l: false, 14 exports: {} 15 }; 16 17 // Execute the module function 18 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 19 20 // Flag the module as loaded 21 module.l = true; 22 23 // Return the exports of the module 24 return module.exports; 25 } 26 27 28 // expose the modules object (__webpack_modules__) 29 __webpack_require__.m = modules; 30 31 // expose the module cache 32 __webpack_require__.c = installedModules; 33 34 // define getter function for harmony exports 35 __webpack_require__.d = function (exports, name, getter) { 36 if (!__webpack_require__.o(exports, name)) { 37 Object.defineProperty(exports, name, {enumerable: true, get: getter}); 38 } 39 }; 40 41 // define __esModule on exports 42 __webpack_require__.r = function (exports) { 43 if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 44 Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); 45 } 46 Object.defineProperty(exports, '__esModule', {value: true}); 47 }; 48 49 // create a fake namespace object 50 // mode & 1: value is a module id, require it 51 // mode & 2: merge all properties of value into the ns 52 // mode & 4: return value when already ns object 53 // mode & 8|1: behave like require 54 __webpack_require__.t = function (value, mode) { 55 if (mode & 1) value = __webpack_require__(value); 56 if (mode & 8) return value; 57 if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 58 var ns = Object.create(null); 59 __webpack_require__.r(ns); 60 Object.defineProperty(ns, 'default', {enumerable: true, value: value}); 61 if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { 62 return value[key]; 63 }.bind(null, key)); 64 return ns; 65 }; 66 67 // getDefaultExport function for compatibility with non-harmony modules 68 __webpack_require__.n = function (module) { 69 var getter = module && module.__esModule ? 70 function getDefault() { 71 return module['default']; 72 } : 73 function getModuleExports() { 74 return module; 75 }; 76 __webpack_require__.d(getter, 'a', getter); 77 return getter; 78 }; 79 80 // Object.prototype.hasOwnProperty.call 81 __webpack_require__.o = function (object, property) { 82 return Object.prototype.hasOwnProperty.call(object, property); 83 }; 84 85 // __webpack_public_path__ 86 __webpack_require__.p = ""; 87 88 89 // Load entry module and return exports 90 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 91 }) 92 /************************************************************************/ 93 ({ 94 "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { 95 "use strict"; 96 97 __webpack_require__.r(__webpack_exports__); 98 var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/module.js"); 99 document.write('index.js loaded.'); 100 Object(_module_js__WEBPACK_IMPORTED_MODULE_0__["default"])(); 101 }), 102 103 "./src/module.js": (function (module, __webpack_exports__, __webpack_require__) { 104 "use strict"; 105 106 __webpack_require__.r(__webpack_exports__); 107 __webpack_exports__["default"] = (function () { 108 document.write('module.js loaded.'); 109 }); 110 }) 111 });
四 源码解读
bundle.js整个代码实际就是一个自执行函数,当在html中加载该文件时,就是执行该自执行函数。
大概结构如下:
1 (function (modules) { 2 // The module cache 3 var installedModules = {}; 4 // The require function 5 function __webpack_require__(moduleId) {...} 6 7 // expose the modules object (__webpack_modules__) 8 __webpack_require__.m = modules; 9 10 // expose the module cache 11 __webpack_require__.c = installedModules; 12 13 // define getter function for harmony exports 14 __webpack_require__.d = function (exports, name, getter) {...}; 15 16 // define __esModule on exports 17 __webpack_require__.r = function (exports) {...}; 18 19 // create a fake namespace object 20 // mode & 1: value is a module id, require it 21 // mode & 2: merge all properties of value into the ns 22 // mode & 4: return value when already ns object 23 // mode & 8|1: behave like require 24 __webpack_require__.t = function (value, mode) {...}; 25 26 // getDefaultExport function for compatibility with non-harmony modules 27 __webpack_require__.n = function (module) {...}; 28 29 // Object.prototype.hasOwnProperty.call 30 __webpack_require__.o = function (object, property) {...}; 31 32 // __webpack_public_path__ 33 __webpack_require__.p = ""; 34 35 // Load entry module and return exports 36 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 37 }) 38 ({ 39 "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {...}), 40 "./src/module.js": (function (module, __webpack_exports__, __webpack_require__) {...}) 41 });
4.1 自执行函数的参数解读
该参数是一个对象,对象属性的key就是入口模块和它引用的所有模块文件的路径和名字组合。整个代码有多少文件被引入,就会有多少个属性对应。属性key对应的值是一个函数。该函数的内容具体是什么,后边会单独分析。
4.2 自执行函数体解读
自执行函数主要做了下边几件事:
1)定义了installedModules缓存模块的对象变量
该变量用于存储被加载过的模块相关信息。该对象的属性结构如下:
1 installedModules = { 2 "./src/index.js": { 3 i: moduleId, 4 l:false, 5 exports: {...} 6 } 7 }
installedModules对象的属性key就是模块的id,跟参数对象的key一样。
属性对象中有三个属性:
i:模块id,目前看和key是一样的。
l:标识该模块是否已经加载过。目前感觉这个变量没啥用,只有加载过的模块才会存到该变量中吧?可能还有其它用途,有待发现。
exports:加载完模块后的,模块导出的值都放在这个变量中。
2)定义了__webpack_require__函数,以及该函数上的各种属性
该函数是Webpack的最核心的函数,类似于RequireJS的require方法。用于文件模块的加载和执行。
详细内容会在下边专门讲到。
3)通过__webpack_require__函数加载入口模块
传入的参数是模块id,"./src/index.js"是入口模块的id标识。在这里正是启动了入口模块的加载。
4.3 __webpack_require__函数源码解读
该函数的源码如下:
1 function __webpack_require__(moduleId) { 2 // Check if module is in cache 3 if (installedModules[moduleId]) { 4 return installedModules[moduleId].exports; 5 } 6 // Create a new module (and put it into the cache) 7 var module = installedModules[moduleId] = { 8 i: moduleId, 9 l: false, 10 exports: {} 11 }; 12 13 // Execute the module function 14 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 15 16 // Flag the module as loaded 17 module.l = true; 18 19 // Return the exports of the module 20 return module.exports; 21 }
该函数主要做了如下几件事:
1)判断该模块是否已经加载过了:如果已经加载过了,从installedModules缓存中找到该模块信息,并将之前加载该模块时保存的exports信息返回。
2)如果该模块没有被加载过:创建一个模块对象,用于保存该模块的信息,并将该模块存储到installedModules缓存变量中,该模块对象的属性详细见前边说明。
3)加载该模块的代码。modules是整个bundle.js文件中自执行函数的参数,详细见上边说明。注意:执行该模块的代码函数时,传入三个参数:模块信息对象、模块导出内容存储对象、__webpack_require__函数。将该函数传入的原因是:加载的当前模块,可能会依赖其它模块,需要__webpack_require__继续加载其它模块。
4)将该模块标识为已加载过的。
5)返回模块的导出值。
4.4 __webpack_require__函数的属性源码解读
该函数上定义了很多属性,各个属性的作用如下(英文是源码的原始注解,这里没有删除):
1 // expose the modules object (__webpack_modules__) 2 // 保存整个所有模块的原始信息,modules是整个bundle.js文件中自执行函数的参数,详细见上边说明。 3 __webpack_require__.m = modules; 4 5 // expose the module cache 6 // 保存所有已加载模块的信息,具体见上边说明 7 __webpack_require__.c = installedModules; 8 9 // define getter function for harmony exports 10 // 工具函数:给对应的exports对象上创建name属性 11 __webpack_require__.d = function (exports, name, getter) { 12 if (!__webpack_require__.o(exports, name)) { 13 Object.defineProperty(exports, name, {enumerable: true, get: getter}); 14 } 15 }; 16 17 // define __esModule on exports 18 // 给缓存中加载过的模块导出对象中,添加__esModule属性。 19 // TODO:具体这个属性的其它用途,待研究 21 __webpack_require__.r = function (exports) { 22 if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { 23 Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); 24 } 25 Object.defineProperty(exports, '__esModule', {value: true}); 26 }; 27 28 // create a fake namespace object 29 // mode & 1: value is a module id, require it 30 // mode & 2: merge all properties of value into the ns 31 // mode & 4: return value when already ns object 32 // mode & 8|1: behave like require 33 // TODO: 待研究该函数作用,后续研究完补充 34 __webpack_require__.t = function (value, mode) { 35 if (mode & 1) value = __webpack_require__(value); 36 if (mode & 8) return value; 37 if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 38 var ns = Object.create(null); 39 __webpack_require__.r(ns); 40 Object.defineProperty(ns, 'default', {enumerable: true, value: value}); 41 if (mode & 2 && typeof value != 'string') 42 for (var key in value) __webpack_require__.d(ns, key, function (key) { 43 return value[key]; 44 }.bind(null, key)); 45 return ns; 46 }; 47 48 // getDefaultExport function for compatibility with non-harmony modules 49 // 工具函数:创建一个获取模块返回值的函数 50 __webpack_require__.n = function (module) { 51 var getter = module && module.__esModule ? 52 function getDefault() { 53 return module['default']; 54 } : 55 function getModuleExports() { 56 return module; 57 }; 58 __webpack_require__.d(getter, 'a', getter); 59 return getter; 60 }; 61 62 // Object.prototype.hasOwnProperty.call 63 // 工具函数:判断一个对象是否存在一个属性 64 __webpack_require__.o = function (object, property) { 65 return Object.prototype.hasOwnProperty.call(object, property); 66 }; 67 68 // __webpack_public_path__ 69 // 基础路径,这个在有些时候非常有用(例如:懒加载时),具体后续补充 70 __webpack_require__.p = "";
通过上边的属性可以看出,通过__webpack_require__函数,可以获取到所有信息。
4.5 入口文件./src/index.js源码解读
上边分析自执行函数中,最主要的一行代码就是通过__webpack_require__函数加载了入口文件。
__webpack_require__(__webpack_require__.s = "./src/index.js")
./src/index.js源码如下:
1 "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { 2 "use strict"; 3 4 __webpack_require__.r(__webpack_exports__); 5 var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/module.js"); 6 document.write('index.js loaded.'); 7 Object(_module_js__WEBPACK_IMPORTED_MODULE_0__["default"])(); 8 })
主要做了如下几件事:
1)调用__webpack_require__.r方法,具体参考上边说明。
2)通过__webpack_require__调用依赖的模块"./src/module.js",并将该模块的exports存入_module_js__WEBPACK_IMPORTED_MODULE_0__ 变量。
3)执行index.js自身代码。
可以看出,相比index.js源码,添加和修改了一些代码,源码中通过ES6的import导入模块方式改为了__webpack_require__方法。
4.6 引用模块./src/module.js源码解读
在上边入口模块index.js中,通过__webpack_require__加载了module.js模块。
该模块源码如下:
1 "./src/module.js": (function (module, __webpack_exports__, __webpack_require__) { 2 "use strict"; 3 4 __webpack_require__.r(__webpack_exports__); 5 __webpack_exports__["default"] = (function () { 6 document.write('module.js loaded.'); 7 }); 8 })
主要做了如下几件事:
1)调用__webpack_require__.r方法,具体参考上边说明。
2)将导出值存入缓存该模块信息对象的exports属性对象中。
到此,bundle.js的所有源码已解读完毕。
五 基础构建&加载原理说明
从上边源码解读中,可以看出,整个构建过程如下:
1.将所有文件和内容存入自执行函数的参数对象;
2.通过__webpack_require__方法加载入口文件;
3.将加载了的文件信息缓存;
4.如果当前加载的文件依赖其它文件,就通过__webpack_require__继续加载其它文件;
5.直到入口文件执行完毕。
下边是一个简单的原理图,画的比较简陋: