Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读

本文主要说明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.直到入口文件执行完毕。

下边是一个简单的原理图,画的比较简陋:

 

posted on 2019-05-29 19:41  ゛墨メ冰ミ  阅读(773)  评论(0编辑  收藏  举报

导航