Open leeword opened 3 years ago
webpack 是一个常用的模块打包器,使用 webpack 将项目代码打包编译后,除业务代码外,还有一部分,则是启动引导程序(webpackBootstrap),它维护着 webpack 内部使用的变量和模块加载逻辑,本文将对这部分内容分析,讲一讲我的理解。
webpack
webpackBootstrap
首先新建 webpack 工程,将 webpackBootstrap 的内容构建到本地:
初始化一个文件夹,在根目录执行 npm init -y命令, 生成 package.json 文件,用于维护项目依赖;
npm init -y
package.json
安装 webpack、webpack-cli (以下使用 webpack 4 为例):
webpack-cli
webpack 4
npm i webpack webpack-cli -D
webpack 默认读取项目根目录下的 webpack.config.js作为配置文件。
webpack.config.js
将 mode 设置为 none,mode 即为模式,配置 mode 的不同值,webpack 会在开发环境或生成环境下,默认启用一些插件,来减少使用者的配置负担。配置 none 则表示,不使用任何 webpack 的任何默认优化,我们研究下最原始版本即可:
mode
none
简单添加一些项目业务代码,方便观察打包内容的变化,示例如下:
这里逻辑比较简单,/src/utils.js 导出了两个函数,然后 /src/index.js 从utils文件导入了 add 函数并执行了它。
/src/utils.js
/src/index.js
utils
add
接下来,我们就可以使用 webpack 对项目工程打包了。在命令行运行下方命令:
npx webpack
完成上述步骤后,项目根目录会多出一个文件夹, dist 目录是webpack打包资源的默认输出目录,/dist/main.js 则是默认输出的文件路径:
dist
/dist/main.js
进行到这里,本篇文章的主角正式登场,我们重点分析下 main.js 的内容,我把它贴出来,下文简称这部分为webpackBootstrap:
main.js
生成的代码并不算特别长,算上注释和空行才100多行,从大致逻辑可以看出,代码主体是一个自执行的 IIFE 函数,我们逐步拆解内容:
IIFE
(function(modules) { // webpack 启动引导代码 })([ (function(module, __webpack_exports__, __webpack_require__) { // 模块 id: 0 对应的业务代码 // src/index.js 对应代码 }), (function(module, __webpack_exports__, __webpack_require__) { // 模块 id: 1 对应的业务代码 // src/utils.js 对应代码 }), ]);
打包阶段,webpack 会将我们的业务代码进行包装,以入口文件为例:
(function(module, __webpack_exports__, __webpack_require__) { // src/index.js 中的代码 }),
业务代码被放入函数体内延迟执行,很好的将业务代码模块的作用域隔离开来。这种做法并不新颖,它借鉴了 NodeJS 代码的执行方式。
NodeJS
在 webpack 内部,每个业务模块使用唯一 id 表示,即 moduleId 。按现有配置 build 出的资源,模块被一一放置在数组里,数组索引具有唯一性,用于表示 moduleId。
id
moduleId
模块被包装后,放在数组里,然后被当成参数传入了 IIFE 函数体。
我们先将不必要的代码剔除掉,只保留主干代码:
(function(modules) { // 保存已加载的模块 var installedModules = {}; // webpack 模块加载函数 function __webpack_require__(moduleId) { // 判断待加载模块是否已有缓存 if (installedModules[moduleId]) { // 如果存在缓存, 返回该模块输出的变量 return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { // 模块id i: moduleId, // 保存模块导出的变量 exports: {} }; // 执行模块包装函数,将三个变量当作参数传入,将模块导出的变量保存在 module.exports 中 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 标记模块已加载 module.l = true; // 若模块初次加载, 返回模块导出的内容 return module.exports; } // 从入口模块开始执行代码 return __webpack_require__(__webpack_require__.s = 0); })([ (function(module, __webpack_exports__, __webpack_require__) { /* 模块 Id: 0 对应的代码 */ }), (function(module, __webpack_exports__, __webpack_require__) { /* 模块 Id: 1 对应的代码 */ }), ...其他模块代码 ]);
代码做了注释帮助读者理解这部分代码,其中__webpack_require__ 是 webpack 中的模块加载函数,本篇着重讲解这部分内容。
__webpack_require__
上文有提到,webpack 模块借鉴了 NodeJS 的包装方式,我们可以把__webpack_require__() 想象成 NodeJS 中的模块引入函数require(),require()的参数是模块路径,__webpack_require__则需要 moduleId。
__webpack_require__()
require()
IIFE 函数体的最后一行:
return __webpack_require__(__webpack_require__.s = 0);
执行了__webpack_require__ 函数并传入参数0,从这里,打包代码正式执行,加载执行模块0
然后执行权转移到 __webpack_require__ 函数内部,加载执行后的代码模块会保存在 installedModules 里
installedModules
首先会进行判断,假如installedModules 存在传入的 moduleId ,则直接返回该模块导出的变量,流程结束:
如果不存在,则以 moduleId 为键名,初始化一个值,保存一些该模块的信息,i 为 moduleId,l 代表模块是否加载,exports存储模块对外暴露的变量:
i
l
exports
紧接着,以 module.exports 为 this 值,执行模块包装函数,并传入三个变量,假如该模块有对外导出的值,将会在函数执行过程中,把对外暴露的接口存储在 module.exports 变量内:
module.exports
this
模块内部代码执行完毕,将模块标记为已加载:
将模块对外导出的变量返回:
模块对外暴露的接口,是如何与 webpack 内部的变量联系到一起的呢? 还记得utils.js对外暴露了两个方法吧,我们来看下编译后模块Id为1的代码(对应源文件/src/utils.js):
utils.js
源代码中以ES module 定义导出的接口,则被编译成了 __webpack_require__.d 函数。
ES module
__webpack_require__.d
包装函数的第二个参数__webpack_exports__ 即为installedModules[moduleId].exports,初始化时是一个空对象,代码如下:
__webpack_exports__
installedModules[moduleId].exports
ES module 模块是静态的,在编译阶段就可以确定模块的依赖顺序。
不过为了兼容浏览器环境和 NodeJS 程序,webpack 会将原生模块导出功能,打包成运行时的导出,每个模块在首次执行时(modules[moduleId].call(...)),通过 __webpack_require__.d 函数,将模块对外暴露的方法,定义为了module.exports的同名对象,这样外部程序就能通过 getter 访问到模块内部的接口了:
modules[moduleId].call(...)
getter
// Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // define getter function for harmony exports // 翻译:为原生 exports 定义 getter 函数 __webpack_require__.d = function(exports, name, getter) { // 若 exports 上找不到 [name] 的 key if(!__webpack_require__.o(exports, name)) { // 在 exports 上定义 key [name] 的 getter 访问函数 Object.defineProperty(exports, name, { enumerable: true, get: getter }); } };
__webpack_require__ 不仅是模块执行器,而且是模块加载器
比如 src/index.js导入了add方法,我们看一下编译后的代码:
src/index.js
在该模块被执行时,__webpack_require__被当作参数传入模块内部,如上所示,使用 __webpack_require__(1) 得到 moduleId 为1的模块(src/utils)对外暴露的代码,它返回了模块1的 exports 引用,然后就可以使用它对外暴露的接口了:Object(_utils__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
__webpack_require__(1)
src/utils
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
在 JavaScript 中,函数是一个特殊的对象,除上文提到的内容,__webpack_require__也提供了几个静态属性:
JavaScript
__webpack_require__.m
__webpack_require__.c
__webpack_require__.r
__esModule
true
__webpack_require__.n
__webpack_require__.o
__webpack_require__.p
publicPath
具体实现参考 webpackBootstrap 。
理解 webpackBootstrap,是理解 webpack 工程的基础。讲到这里,很多问题都有了答案,比如为了提升hash 稳定性,为什么要把 moduleId 从数组索引替换为模块路径。
hash
上文讲解了同步模块的保存、引用、加载逻辑,如果项目内部使用import()对项目代码做了动态分割,webpackBootstrap 内容会有变化么?这部分留给读者自行探索了。
import()
webpack
是一个常用的模块打包器,使用webpack
将项目代码打包编译后,除业务代码外,还有一部分,则是启动引导程序(webpackBootstrap
),它维护着webpack
内部使用的变量和模块加载逻辑,本文将对这部分内容分析,讲一讲我的理解。新建工程
首先新建
webpack
工程,将webpackBootstrap
的内容构建到本地:初始化一个文件夹,在根目录执行
npm init -y
命令, 生成package.json
文件,用于维护项目依赖;安装
webpack
、webpack-cli
(以下使用webpack 4
为例):webpack
默认读取项目根目录下的webpack.config.js
作为配置文件。将
mode
设置为none
,mode
即为模式,配置mode
的不同值,webpack
会在开发环境或生成环境下,默认启用一些插件,来减少使用者的配置负担。配置none
则表示,不使用任何webpack
的任何默认优化,我们研究下最原始版本即可:简单添加一些项目业务代码,方便观察打包内容的变化,示例如下:
这里逻辑比较简单,
/src/utils.js
导出了两个函数,然后/src/index.js
从utils
文件导入了add
函数并执行了它。接下来,我们就可以使用
webpack
对项目工程打包了。在命令行运行下方命令:完成上述步骤后,项目根目录会多出一个文件夹,
dist
目录是webpack
打包资源的默认输出目录,/dist/main.js
则是默认输出的文件路径:webpack bootStrap
进行到这里,本篇文章的主角正式登场,我们重点分析下
main.js
的内容,我把它贴出来,下文简称这部分为webpackBootstrap
:生成的代码并不算特别长,算上注释和空行才100多行,从大致逻辑可以看出,代码主体是一个自执行的
IIFE
函数,我们逐步拆解内容:打包阶段,
webpack
会将我们的业务代码进行包装,以入口文件为例:业务代码被放入函数体内延迟执行,很好的将业务代码模块的作用域隔离开来。这种做法并不新颖,它借鉴了
NodeJS
代码的执行方式。在
webpack
内部,每个业务模块使用唯一id
表示,即moduleId
。按现有配置 build 出的资源,模块被一一放置在数组里,数组索引具有唯一性,用于表示moduleId
。模块被包装后,放在数组里,然后被当成参数传入了
IIFE
函数体。我们先将不必要的代码剔除掉,只保留主干代码:
代码做了注释帮助读者理解这部分代码,其中
__webpack_require__
是webpack
中的模块加载函数,本篇着重讲解这部分内容。上文有提到,
webpack
模块借鉴了NodeJS
的包装方式,我们可以把__webpack_require__()
想象成NodeJS
中的模块引入函数require()
,require()
的参数是模块路径,__webpack_require__
则需要moduleId
。IIFE
函数体的最后一行:return __webpack_require__(__webpack_require__.s = 0);
执行了
__webpack_require__
函数并传入参数0,从这里,打包代码正式执行,加载执行模块0然后执行权转移到
__webpack_require__
函数内部,加载执行后的代码模块会保存在installedModules
里首先会进行判断,假如
installedModules
存在传入的moduleId
,则直接返回该模块导出的变量,流程结束:如果不存在,则以
moduleId
为键名,初始化一个值,保存一些该模块的信息,i
为moduleId
,l
代表模块是否加载,exports
存储模块对外暴露的变量:紧接着,以
module.exports
为this
值,执行模块包装函数,并传入三个变量,假如该模块有对外导出的值,将会在函数执行过程中,把对外暴露的接口存储在module.exports
变量内:模块内部代码执行完毕,将模块标记为已加载:
将模块对外导出的变量返回:
模块定义对外接口
模块对外暴露的接口,是如何与
webpack
内部的变量联系到一起的呢? 还记得utils.js
对外暴露了两个方法吧,我们来看下编译后模块Id为1的代码(对应源文件/src/utils.js
):源代码中以
ES module
定义导出的接口,则被编译成了__webpack_require__.d
函数。包装函数的第二个参数
__webpack_exports__
即为installedModules[moduleId].exports
,初始化时是一个空对象,代码如下:ES module
模块是静态的,在编译阶段就可以确定模块的依赖顺序。不过为了兼容浏览器环境和
NodeJS
程序,webpack
会将原生模块导出功能,打包成运行时的导出,每个模块在首次执行时(modules[moduleId].call(...)
),通过__webpack_require__.d
函数,将模块对外暴露的方法,定义为了module.exports
的同名对象,这样外部程序就能通过getter
访问到模块内部的接口了:加载引用外部模块
__webpack_require__
不仅是模块执行器,而且是模块加载器比如
src/index.js
导入了add
方法,我们看一下编译后的代码:在该模块被执行时,
__webpack_require__
被当作参数传入模块内部,如上所示,使用__webpack_require__(1)
得到moduleId
为1的模块(src/utils
)对外暴露的代码,它返回了模块1的exports
引用,然后就可以使用它对外暴露的接口了:Object(_utils__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2)
__webpack_require__
的其他属性在
JavaScript
中,函数是一个特殊的对象,除上文提到的内容,__webpack_require__
也提供了几个静态属性:__webpack_require__.m
:保存打包好同步的模块__webpack_require__.c
:保存已加载的模块__webpack_require__.d
:为原生模块导出语法定义getter
__webpack_require__.r
:如果模块是ES模块,在exports
上定义__esModule
值为true
__webpack_require__.n
:为了和非ES模块兼容,定义模块导出兼容函数__webpack_require__.o
:检测对象属性是否在对象上存在,定义简写写法__webpack_require__.p
:保存publicPath
的值具体实现参考
webpackBootstrap
。小结
理解
webpackBootstrap
,是理解webpack
工程的基础。讲到这里,很多问题都有了答案,比如为了提升hash
稳定性,为什么要把moduleId
从数组索引替换为模块路径。上文讲解了同步模块的保存、引用、加载逻辑,如果项目内部使用
import()
对项目代码做了动态分割,webpackBootstrap
内容会有变化么?这部分留给读者自行探索了。