Webpack核心流程
Webpack的最核心的功能:静态模块打包能力 ,Webpack是现代JavaScript应用程序的静态模块打包工具。Webpack 的核心思想是 "一切皆模块",传统来讲,模块指的就是一段可复用的代码,在Webpack中,任何类型的资源都会被视为一个模块,好处是统一前端资源的管理方式。
Webpack核心功能中提到了"静态"二字,该如何理解?
什么是静态?
Webpack的核心工作原理是:在构建阶段(而非运行时),通过静态分析确定所有模块的依赖关系。
静态分析:通过将源代码转换为AST,对AST遍历和分析。
静态分析特点:
- 源代码中必须明确依赖(import,require)。
- 依赖路径必须是静态字符串,不能是动态字符串,如通过用户输入的变量拼接而成等。
js
// 动态:路径依赖变量,只有运行时才知道具体加载哪个模块
const moduleName = getModuleName(); // 函数返回值在运行时确定
const module = require(`./modules/${moduleName}`);
// 静态
import './a.js'
通俗来说,就是代码之间的依赖关系要在代码运行前就确定好。比如说,如果引用路径里包含变量,这种情况下,只有等代码运行起来,知道了变量的具体值,才能确定实际的引用路径,这就不符合这种提前确定依赖的要求了。这个例子的描述就可以感受到Webpack概念中关于"静态"的理解。
为什么采用静态分析而不是动态分析 :
静态分析能在构建阶段就梳理清楚所有依赖关系 ,最终将代码打包成浏览器可直接执行的形式。这样一来,代码运行时就无需再处理依赖相关的逻辑,只需专注于响应用户操作即可。
而动态分析则意味着浏览器在运行代码的过程中,还需要实时解析依赖关系并加载相应模块,这无疑会增加页面的加载时间和运行时开销。
Webpack模块
Webpack 能够将各种类型的资源 ------ 包括图片、音视频、CSS、JavaScript 代码等,通通转译、组合、拼接、生成标准的、能够在不同版本浏览器兼容执行的 JavaScript 代码文件,这一特性能够轻易抹平开发 Web 应用时处理不同资源的逻辑差异,使得开发者以一致的心智模型开发、消费这些不同的资源文件。在构建流程中的模块的具体表现就是module
对象,module
是构建流程的核心数据,后便会多次提到。
CSS文件经过css-loader
处理:
css
源CSS代码:
body {
background: #fff;
color: #333;
}
.title {
font-size: 20px;
}
经过转换之后的:
// 模块id假设为3
__webpack_modules__[3] = (module, exports, __webpack_require__) => {
// 导出一个对象,包含CSS内容及模块信息
exports = module.exports = __webpack_require__(/* ! css-loader */).default;
// 核心:CSS内容以数组形式存储,包含模块id、CSS字符串、媒体查询(空表示无)
exports.push([ module.id, "body {\n background: #fff;\n color: #333;\n}\n.title {\n font-size: 20px;\n}", "" ]);
};
小图片经过url-loader
进行处理(base64)
js
// 模块id假设为4,图片被转为了base64的格式
__webpack_modules__[4] = (module, exports) => {
// 导出base64编码的图片字符串
module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAA...";
};
大图片经过file-loader
进行处理(文件路径)
javascript
// 模块id假设为5
__webpack_modules__[5] = (module, exports) => {
// 导出图片的输出路径(由file-loader生成,含hash防止缓存)
module.exports = "./dist/images/banner.8a3b2d.png";
};
可以看到上述的非JavaScript的内容都会转换为JavaScript代码:
- 统一了导入导出的逻辑,用一致的方式处理所有的资源。
- 同时转换为JavaScript代码也为后续的AST分析依赖打下基础。
Webpack核心流程
Webpack主要流程就是从入口文件出发,分析模块(文件)之间的依赖,最终打包输出文件产物,整个流程可以分为三个步骤。

初始化阶段 :合并配置参数创建Compiler
对象,并注册插件。
- 合并配置参数,包括Webpack内部默认参数,
process.args
参数 和webpack.config.js
文件,创建Compiler
对象。 - 遍历配置中的
plugins
数组,注册插件,调用插件的apply
方法,方法中会调用插件系统注册的钩子函数,当构建流程走到相应的阶段,会触发插件的回调代码。
Compliler :代表Webpack实例,是全局唯一,贯穿整个生命周期,Compliler
的作用是保存全局参数,提供钩子机制,允许插件进行扩展。
从形态上看,插件通常是一个带有 apply
函数的类,如:
js
class SomePlugin {
apply(compiler) {
}
}
在初始化阶段 注册插件会调用插件对象的apply
方法,并以参数的形式传递Compiler
对象,前边我们说过Compiler
对象提供了钩子机制,所可以通过Compiler
对象注册Webpack的钩子回调。
Webpack的钩子机制可以类比于Vue
的生命周期,在打包的过程中,会经历很多阶段,有些阶段会触发过回调函数。下边是例子:
js
class SomePlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("SomePlugin", (compilation) => {
compilation.addModule(/* ... */);
});
}
}
- 在初始化注册插件阶段,会调用
SomePlugin
插件的apply
方法。 - 插件通过
compiler.hooks.thisCompilation.tap
注册了一个回调函数,绑定在thisCompilation
钩子上。 - 当Webpack构建流程到达
thisCompilation
(在当前编译流程(Compilation
实例)被创建后、但还未开始处理模块和依赖前触发)会触发回调函数,在回调函数中会调用compilation.addModule(/* ... */)
。
compilation
是「单次编译的执行者」,每次构建(包括watch
模式下的重新构建)都会创建一个新的,通过compilation
也能注册一些钩子的回调函数。
构建阶段 :将项目资源转换为module
,并通过AST
分析module
的依赖关系,构建依赖图谱moduleGraph
。
EntryPlugin
监听compiler.hook.make
钩子并开始调用compilation.addEntry
添加入口文件,将入口文件转换为dependency
依赖。- 之后会调用
handleModuleCreation
,根据文件类型构建module
子类,此时的module
是一个"空壳",仅包含模块路径、类型等元信息。 - 使用
loader-runner
根据不同类型的module
(.css, .svg)按顺序执行loader
链,对模块的原始内容 (磁盘文件内容)进行转换为JavaScript代码。然后会回填到module
实例中。 - 使用
acorn
将JavaScript代码转换为AST
,遍历AST
,遇到import
和export
则会创建相应的dependency
(一个及以上),然后加入到module
的依赖数组中。 - 对于
module
新增的dependency
依赖,会重新回到第二步继续处理。 - 所有的依赖解析完毕,构建阶段结束。
上述流程提到了dependency
,dependency
代表的就是module
与module
之间的依赖关系。
上述过程中模块源码经历了 module => ast => dependences => module
的流转。
生成阶段 :根据用户配置或者默认的分包规则将module
组合成chunk
并构建chunk
与chunk
关系图谱chunkGraph
,然后以chunk
粒度将源码转译为目标环境可以运行的产物并进行输出。
生成阶段 又可以分为三个阶段 ,分别是构建chunkGraph
, 优化chunkGraph
和 转换,注入运行时代码并输出文件。
chunk是什么?
在 Webpack 构建流程中,三者的流转关系是:
module
(分散的模块)→ chunk
(模块的分组)→ asset
(最终输出的文件)
chunk
是一组相关模块module
的组合,是对模块module
进行逻辑分组,chunk
的出现给对module
的组合优化留下了空间。正因为模块被分组为 chunk
,Webpack 才能在 "组" 的维度上实施优化(如提取公共 chunk 避免重复、对异步 chunk 实现按需加载等)
构建/优化chunkGraph
chunkGraph
是chunk的依赖图谱,这个阶段就是创建chunk
,然后建立chunk
与chunk
之间的关系,chunk
的默认分包规则是:
- Entry Chunk:一个
entry
入口文件能够触达到的所有module
会组合成一个chunk
。 - Async Chunk: 异步加载的
module
及能够触达到的module
会组合成一个chunk
。(优化阶段) - Runtime Chunk:配置了
entry.runtime
,就会为入口文件单独打包一个chunk
,如有两个入口文件,并且配置了不同的runtime
值,那么就会生成两个Runtime Chunk
,如果配置相同则会生成一个Runtime Chunk
。(优化阶段)
Async Chunk的例子:
import('./async-a.js')
chunk
与chunk
之间也存在依赖关系,如Entry Chunk
与Runtime Chunk
之间就是父子关系,将这些依赖关系记录下来并存到ChunkGraph
中。
该流程大致可以总结为 :初始阶段 Webpack 会基于入口配置创建「初始 entry chunk」(包含入口模块及同步依赖),而异步 chunk 和 runtime chunk 是在优化阶段通过「代码分割」从初始 chunk 或依赖图中拆分 / 提取出来的,本质上是对模块资源的重新分配。
优化阶段会触发optimizeModules/optimizeChunks
钩子,由插件(SplitChunksPlugin)进一步修剪,优化chunk结构,如果用户没有注册相关的插件,则上一步构建的chunkGraph
就是最终的结构了。
转换,注入运行时代码并输出文件
该阶段大概可以分为三个步骤。
-
遍历
module
对象,对每个module
源码进行转换。- 在构建阶段,
module
对象的依赖列表dependencies
已经收集了很多的依赖dependency
,每个dependency
都对应着一个template.apply
方法。调用每个dependency
对象的template.apply
方法,可能会产生三种副作用。- 直接修改
module
的source
,相当于直接修改源码。 - 会将修改结果记录到
initFragments
数组中,initFragments
数组储存的都是需要插入到源码中的代码片段,遍历完dependency
数组后会将initFragments
数组中的代码片段插入到源码中。 - 将运行时依赖记录到
runtimeRequirements
数组中。
- 直接修改
- 在构建阶段,
-
以
chunk
维度收集运行时依赖- 在构建阶段分析AST的时候,解析到模块化语句,会创建了对应的运行时
dependency
依赖。 - 在上一个步骤对
module
的处理中,会将Runtime Dependency
转换为__webpack_require__
等枚举值,runtimeRequirements
数组中存储的就是枚举值。 - 会遍历
chunk
中的所有module
的runtimeRequirements
,并把运行时依赖集中起来,并把该数据记录到chunkGraph
中。
- 在构建阶段分析AST的时候,解析到模块化语句,会创建了对应的运行时
-
合并最终产物
- 合并 CMD 代码;
- 合并 runtime 模块代码;
- 遍历
chunkModules
变量,合并除 entry 外其它模块代码; - 合并 entry 模块代码。 后续调用
compilation.emitAsset
,最终将产物内容输出为指定路径的文件中。
流程总结
从流程来看Webpack可以分为三个步骤:初始化 -> 编译 -> 生成 。
从资源流转来看可以分为:源码
-> dependency
-> module
-> chunk
-> asset
-> 文件