📦 详细打包流程解析
本文详细解析一下Webpack详细打包流程,包含与常用loader和Plugin的交互。
直接上图

分阶段解析
第1阶段:初始化(Initialization)
触发调用 :当执行 webpack 命令或调用 webpack(config) API 时启动。
参数交互:
- 读取
webpack.config.js配置对象,与命令行参数合并 - 接收参数:
config(配置对象)、callback(可选回调函数) - 输出对象:创建
Compiler实例并返回
关键过程:
- 配置合并:将用户配置、CLI参数和Webpack默认配置深度合并,形成最终配置对象
- Compiler创建:实例化Compiler类,这是Webpack的核心控制类,继承自Tapable,提供钩子系统
- Plugin实例化 :遍历
config.plugins数组,对每个插件调用new Plugin()创建实例 - Plugin注册 :对每个插件实例调用
apply(compiler)方法,插件通过此方法订阅生命周期钩子 - DefinePlugin工作 :在
compiler.options中注入全局常量,如process.env.NODE_ENV = 'production' - 环境准备 :根据
mode配置,Webpack内部加载对应环境的内置插件
涉及Plugin :DefinePlugin 在此阶段通过修改 compiler.options 定义全局常量。
第2阶段:编译准备(Compilation Preparation)
触发调用 :调用 compiler.run() 或 compiler.watch() 启动构建过程。
钩子触发顺序:
- beforeRun:清理缓存等准备工作
- run:开始执行编译
- compile:创建Compilation对象前
- thisCompilation:初始化Compilation对象时
- compilation:Compilation对象创建完成
- make:开始从入口构建依赖图
关键过程:
- 钩子触发:Compiler按顺序触发各生命周期钩子,插件可以监听这些钩子执行自定义逻辑
- Compilation创建 :在
compile钩子后创建Compilation实例,它代表一次独立的构建过程 - 入口解析 :EntryPlugin监听
make钩子,根据entry配置解析入口文件路径 - 模块工厂创建:为不同类型的模块(NormalModule, ContextModule等)创建对应的ModuleFactory
参数传递 :compilation 对象被传递给所有监听 compilation 钩子的插件,包含本次构建的所有上下文信息。
第3阶段:模块处理(Module Processing)
触发调用:由NormalModuleFactory在解析模块时触发。
Loader执行机制:
-
匹配规则 :根据
module.rules配置的test、include、exclude匹配文件 -
执行顺序:对匹配的规则,Loader从右到左、从下到上执行(pitch阶段从左到右)
-
参数传递:每个Loader接收上一个Loader处理后的内容或原始文件内容
解释一下这个执行顺序:
loader分两个阶段:Pitch函数(预处理)、Normal函数(主逻辑)
假如配置:
{
test: /test.js$/,
use: [
'./loaders/loader1.js',
'./loaders/loader2.js',
'./loaders/loader3.js'
}
执行链路为:loader1(pitch) -> loader2(pitch) -> loader3(pitch) -> loader3(normal) -> loader2(normal) -> loader1(normal)。
Pitch阶段: 各loader传递一个"如何找到并处理这个文件"的计划描述:字符串路径和请求描述
Normal阶段:最后一个loader(最右边)接收原始文件内容,第一个loader(最左边)输出最终结果
这种设计让loader可以:- 在pitch阶段进行前置检查或优化
- 通过熔断机制跳过不必要的处理
- 通过共享data对象传递元信息
- 形成清晰的处理管道,每个loader职责单一
实际开发中,style-loader就利用了这个机制:它的pitch函数直接返回一段JS代码,将CSS注入到页面,从而避免CSS文件再经过其他loader处理。
记忆技巧:
Pitch是"从左到右"的准备阶段,可以做检查、熔断、设置数据
Normal是"从右到左"的处理阶段,实际转换文件内容
Pitch像"请求",Normal像"响应",形成完整的处理闭环
不同类型模块的处理流程:
Vue文件(vue-loader):
- 接收Vue SFC(单文件组件)原始内容
- 解析为 descriptor 对象,包含 template、script、styles 块
- 对每个块生成虚拟模块,如
App.vue?vue&type=template&id=xxxx - 各虚拟模块重新进入Loader匹配流程,分别处理
SCSS文件处理链:
javascript
// Loader链:从右到左执行
[
'style-loader', // 将CSS注入DOM
'css-loader', // 解析@import和url()
'postcss-loader', // 添加浏览器前缀等
'sass-loader' // 将SCSS编译为CSS
]
// 或者使用 MiniCssExtractPlugin.loader 替代 style-loader 提取CSS
JS/JSX/TS文件(babel-loader):
- 调用
@babel/core的transformSync或transformAsync方法 - 读取
.babelrc、babel.config.js或 package.json 中的babel配置 - 应用presets(如@babel/preset-env、@babel/preset-react)和plugins
- 生成转译后的ES5代码和source map
资源文件(file-loader/url-loader):
url-loader:比较文件大小与limit配置- 小于limit:转换为Base64 Data URL,格式:
data:image/png;base64,... - 大于limit:回退到
file-loader
- 小于limit:转换为Base64 Data URL,格式:
file-loader:复制文件到输出目录,返回新的公共URL路径
模块对象创建:每个模块处理后,创建NormalModule实例,包含:
resource:模块绝对路径loaders:应用的Loader数组rawRequest:原始请求字符串dependencies:依赖数组
第4阶段:依赖分析(Dependency Analysis)
触发调用:在Module构建完成后,由ModuleGraph模块触发依赖分析。
AST解析过程:
- 解析器调用 :使用
@babel/parser(原babylon)或acorn解析JavaScript代码 - AST遍历 :使用
@babel/traverse或自定义遍历器访问AST节点 - 依赖识别 :
- 识别
import语句(ES6模块) - 识别
require调用(CommonJS) - 识别
require.resolve、import()动态导入 - 识别CSS中的
@import、url()(由css-loader处理)
- 识别
- 依赖收集:将识别到的依赖路径转换为绝对路径,添加到模块的dependencies数组
递归处理:
- 创建依赖队列,初始包含入口模块
- 循环处理队列中的每个模块
- 对每个模块进行第3阶段的Loader处理
- 解析处理后的代码,提取新依赖加入队列
- 重复直到队列为空
依赖关系图:最终构建出完整的ModuleGraph,包含:
- 所有模块的元数据
- 模块间的依赖关系(父子、兄弟关系)
- 模块的导出和引用信息
第5阶段:封装优化(Sealing and Optimization)
触发调用 :当所有模块构建完成后,Compilation调用 seal() 方法。
钩子触发顺序:
- finishModules:所有模块构建完成
- seal:开始封装过程
- optimizeDependencies:依赖优化
- afterOptimizeDependencies:依赖优化后
- beforeChunks / afterChunks:Chunk生成前后
- optimize:优化阶段开始
- optimizeModules / afterOptimizeModules:模块优化
- optimizeChunks / afterOptimizeChunks:Chunk优化
- optimizeTree / afterOptimizeTree:依赖树优化
- optimizeChunkAssets / afterOptimizeChunkAssets:资源优化
关键过程:
Chunk生成:
- 根据入口模块创建初始Chunk
- 识别动态导入(
import())创建异步Chunk - 构建ChunkGraph,表示Chunk间的依赖关系
SplitChunksPlugin工作:
- 分析所有Chunk中的模块
- 根据
optimization.splitChunks配置:chunks: 'all'分析所有ChunkminSize: 20000最小20KB才提取cacheGroups配置缓存组,如将node_modules提取到vendors
- 提取符合条件的公共模块到独立Chunk
Tree Shaking:
- 标记阶段:遍历所有模块,标记未使用的导出(在production模式下自动启用)
- 摇摆阶段:分析副作用,
package.json中的sideEffects字段影响此过程 - 清除阶段:在代码压缩时移除未使用的代码
代码压缩:
- TerserPlugin :调用terser库压缩JavaScript,接收参数:
parallel: true启用多进程terserOptions传递压缩选项
- CssMinimizerWebpackPlugin:使用cssnano压缩CSS
资产生成:将每个Chunk转换为最终资源(Asset),包含:
- 生成文件名(应用
[contenthash]等占位符) - 应用模板生成最终代码(如将模块包装到webpack bootstrap代码中)
第6阶段:输出文件(Output)
触发调用 :在 seal() 完成后,Compiler调用 emitAssets()。
钩子触发顺序:
- afterCompile:编译完成
- shouldEmit / emit:准备输出资产
- afterEmit:资产输出后
- done / failed:构建完成或失败
关键过程:
CleanWebpackPlugin工作:
- 监听
emit或beforeRun钩子 - 读取
output.path配置 - 删除目录中的所有文件(或匹配模式的文件)
- 可配置
dry: false(实际删除)、verbose: true(显示删除日志)
资产写入:
- 遍历
compilation.assets对象 - 对每个资产,调用
fs.writeFile写入文件系统 - 应用
output.filename和output.chunkFilename的命名规则 - 生成source map文件(如果配置了
devtool)
HtmlWebpackPlugin工作:
- 监听
emit钩子,在资产写入前执行 - 读取模板文件(默认或自定义)
- 遍历
compilation.chunks,收集所有JS和CSS资产 - 使用
html-webpack-plugin的模板引擎将资产注入HTML - 将生成的HTML添加到
compilation.assets中 - 支持多页面应用(多次实例化插件)
路径注入示例:
html
<!-- 输入模板 -->
<!DOCTYPE html>
<html>
<head>
<%= htmlWebpackPlugin.options.title %>
</head>
<body>
<div id="root"></div>
</body>
</html>
<!-- 输出HTML -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<link href="main.abc123.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script src="runtime.def456.js"></script>
<script src="vendors.ghi789.js"></script>
<script src="main.abc123.js"></script>
</body>
</html>
第7阶段:完成与分析(Completion and Analysis)
触发调用:资产写入文件系统后,Compiler触发完成钩子。
钩子参数:
done钩子接收stats对象,包含构建统计信息failed钩子接收error对象
webpack-bundle-analyzer工作:
- 监听
done钩子,接收stats对象 - 从
stats中提取bundle信息 - 启动本地服务器(默认端口8888)
- 生成可视化图表,展示:
- 每个bundle的体积
- 模块组成结构
- 模块大小分布
- 重复模块检测
- 支持交互:缩放、搜索、筛选
开发模式特殊处理:
- HotModuleReplacementPlugin :
- 在
compiler.hooks上注册多个钩子监听器 - 建立WebSocket连接,监听文件变化
- 实现增量更新机制
- 在
- webpack-dev-server :
- 提供开发服务器
- 实现内存文件系统(不写入磁盘)
- 支持代理、historyApiFallback等特性
Stats对象:包含完整的构建信息,可用于:
- 控制台输出格式化
- 生成JSON报告
- 自定义分析工具
- 性能监控
整个Webpack构建流程通过这些阶段的精密配合,将源代码转换为可部署的静态资源,同时提供了丰富的扩展点供Loader和Plugin介入,形成了强大的前端构建生态系统。