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

时间:2023-03-08 16:15:26

本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析。

一 说明

本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理。

本文使用的Webpack版本是4.32.2版本。

注意:之前也分析过Webpack3.10.0版本构建出来的bundle.js,通过和这次的Webpack 4.32.2版本对比,核心的构建原理基本一致,只是将模块索引id改为文件路径和名字、模块代码改为了eval(moduleString)执行的方式等一些优化改造。

二 示例

1)Webpack.config.js文件内容:

 const path = require('path');

 module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development' // 'production' 用于配置开发还是发布模式
};

2)创建src文件夹,添加入口文件index.js:

 import moduleLog from './module.js';

 document.write('index.js loaded.');

 moduleLog();

3)在src目录下创建module.js文件:

 export default function () {
document.write('module.js loaded.');
}

4)package.json文件内容:

 {
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"webpack": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2"
},
"dependencies": {
"lodash": "^4.17.4"
}
}

三 执行构建

执行构建命令:npm run webpack

在dist目录下生成的bundle.js源码如下(下边代码是将注释去掉、压缩的代码还原后的代码):

 (function (modules) {
// The module cache
var installedModules = {};
// The require function
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__)
__webpack_require__.m = modules; // expose the module cache
__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, __webpack_exports__, __webpack_require__) {
"use strict"; __webpack_require__.r(__webpack_exports__);
var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/module.js");
document.write('index.js loaded.');
Object(_module_js__WEBPACK_IMPORTED_MODULE_0__["default"])();
}), "./src/module.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict"; __webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (function () {
document.write('module.js loaded.');
});
})
});

四 源码解读

bundle.js整个代码实际就是一个自执行函数,当在html中加载该文件时,就是执行该自执行函数。

大概结构如下:

 (function (modules) {
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {...} // expose the modules object (__webpack_modules__)
__webpack_require__.m = modules; // expose the module cache
__webpack_require__.c = installedModules; // define getter function for harmony exports
__webpack_require__.d = function (exports, name, getter) {...}; // define __esModule on exports
__webpack_require__.r = function (exports) {...}; // 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) {...}; // getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function (module) {...}; // Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (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, __webpack_exports__, __webpack_require__) {...}),
"./src/module.js": (function (module, __webpack_exports__, __webpack_require__) {...})
});

4.1 自执行函数的参数解读

该参数是一个对象,对象属性的key就是入口模块和它引用的所有模块文件的路径和名字组合。整个代码有多少文件被引入,就会有多少个属性对应。属性key对应的值是一个函数。该函数的内容具体是什么,后边会单独分析。

4.2 自执行函数体解读

自执行函数主要做了下边几件事:

1)定义了installedModules缓存模块的对象变量

该变量用于存储被加载过的模块相关信息。该对象的属性结构如下:

 installedModules = {
"./src/index.js": {
i: moduleId,
l:false,
exports: {...}
}
}

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__函数源码解读

该函数的源码如下:

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

该函数主要做了如下几件事:

1)判断该模块是否已经加载过了:如果已经加载过了,从installedModules缓存中找到该模块信息,并将之前加载该模块时保存的exports信息返回。

2)如果该模块没有被加载过:创建一个模块对象,用于保存该模块的信息,并将该模块存储到installedModules缓存变量中,该模块对象的属性详细见前边说明。

3)加载该模块的代码。modules是整个bundle.js文件中自执行函数的参数,详细见上边说明。注意:执行该模块的代码函数时,传入三个参数:模块信息对象、模块导出内容存储对象、__webpack_require__函数。将该函数传入的原因是:加载的当前模块,可能会依赖其它模块,需要__webpack_require__继续加载其它模块。

4)将该模块标识为已加载过的。

5)返回模块的导出值。

4.4 __webpack_require__函数的属性源码解读

该函数上定义了很多属性,各个属性的作用如下(英文是源码的原始注解,这里没有删除):

 // expose the modules object (__webpack_modules__)
// 保存整个所有模块的原始信息,modules是整个bundle.js文件中自执行函数的参数,详细见上边说明。
__webpack_require__.m = modules; // expose the module cache
// 保存所有已加载模块的信息,具体见上边说明
__webpack_require__.c = installedModules; // define getter function for harmony exports
// 工具函数:给对应的exports对象上创建name属性
__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
// 给缓存中加载过的模块导出对象中,添加__esModule属性。
19 // TODO:具体这个属性的其它用途,待研究
__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
// TODO: 待研究该函数作用,后续研究完补充
__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 = "";

通过上边的属性可以看出,通过__webpack_require__函数,可以获取到所有信息。

4.5 入口文件./src/index.js源码解读

上边分析自执行函数中,最主要的一行代码就是通过__webpack_require__函数加载了入口文件。

__webpack_require__(__webpack_require__.s = "./src/index.js")

./src/index.js源码如下:

 "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict"; __webpack_require__.r(__webpack_exports__);
var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/module.js");
document.write('index.js loaded.');
Object(_module_js__WEBPACK_IMPORTED_MODULE_0__["default"])();
})

主要做了如下几件事:

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模块。

该模块源码如下:

 "./src/module.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict"; __webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (function () {
document.write('module.js loaded.');
});
})

主要做了如下几件事:

1)调用__webpack_require__.r方法,具体参考上边说明。

2)将导出值存入缓存该模块信息对象的exports属性对象中。

到此,bundle.js的所有源码已解读完毕。

五 基础构建&加载原理说明

从上边源码解读中,可以看出,整个构建过程如下:

1.将所有文件和内容存入自执行函数的参数对象;

2.通过__webpack_require__方法加载入口文件;

3.将加载了的文件信息缓存;

4.如果当前加载的文件依赖其它文件,就通过__webpack_require__继续加载其它文件;

5.直到入口文件执行完毕。

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

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