详细解析Webpack是怎么运行的

2020-02-03 16:01:32王振洲

我们按照 ES 规范的模块化语法写了一个简单的模块 src/utils/math.js,给 src/index.js 引用。Webpack 用自己的方式支持了 ES6 Module 规范,前面提到的 module 就是和 ES6 module 对应的概念。

接下来我们看一下这些模块是如何通 ES5 代码实现的。再次运行命令 webpack --config webpack.config.js 后查看输出文件:

(function(modules){
 // ...
})({
 "./src/index.js": (function(){
 // ...
 }),
 "./src/utils/math.js": (function() {
 // ...
 })
});

IIFE 传入的 modules 对象里多了一个键值对,对应着新模块 src/utils/math.js,这和我们在源代码中拆分的模块互相呼应。然而,有了 modules 只是第一步,这份文件最终达到的效果应该是让各个模块按开发者编排的顺序运行。

探究 webpackBootstrap

接下来看看 webpackBootstrap 函数中有些什么:

// webpackBootstrap
(function(modules){
 // 缓存 __webpack_require__ 函数加载过的模块
 var installedModules = {};
 /**
 * Webpack 加载函数,用来加载 webpack 定义的模块
 * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js"
 * @returns {Object} exports 导出对象
 */
 function __webpack_require__(moduleId) {
 // ...
 }
 // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ...
 // 传入表达式的值为 "./src/index.js"
 return __webpack_require__(__webpack_require__.s = "./src/index.js");
})(/* modules */);

可以看到其实主要做了两件事:

定义一个模块加载函数__webpack_require__。

使用加载函数加载入口模块 "./src/index.js"。

整个 webpackBootstrap 中只出现了入口模块的影子,那其他模块又是如何加载的呢?我们顺着 __webpack_require__("./src/index.js") 细看加载函数的内部逻辑:

function __webpack_require__(moduleId) {
 // 重复加载则利用缓存
 if (installedModules[moduleId]) {
 return installedModules[moduleId].exports;
 }
 // 如果是第一次加载,则初始化模块对象,并缓存
 var module = installedModules[moduleId] = {
 i: moduleId, // 模块 ID
 l: false,  // 模块加载标识
 exports: {} // 模块导出对象
 };
 /**
 * 执行模块
 * @param module.exports -- 模块导出对象引用,改变模块包裹函数内部的 this 指向
 * @param module -- 当前模块对象引用
 * @param module.exports -- 模块导出对象引用
 * @param __webpack_require__ -- 用于在模块中加载其他模块
 */
 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 // 模块加载标识置为已加载
 module.l = true;
 // 返回当前模块的导出对象引用
 return module.exports;
}

首先,加载函数使用了闭包变量 installedModules,用来将已加载过的模块保存在内存中。 接着是初始化模块对象,并把它挂载到缓存里。然后是模块的执行过程,加载入口文件时 modules[moduleId] 其实就是./src/index.js对应的模块函数。执行模块函数前传入了跟模块相关的几个实参,让模块可以导出内容,以及加载其他模块的导出。最后标识该模块加载完成,返回模块的导出内容。