6-1 webpack工作流程:loaders&plugins执行流程解析
前置知识:tapable 的意义
前面几节花费大量篇幅介绍 tapable,原因是 Webpack 的整个构建流程就是基于 tapable 的事件驱动架构。理解了 tapable 的 Hook 机制,再看 Webpack 的 Loader 和 Plugin 执行流程就一目了然了。
查看 Webpack 官方文档的 Plugin 页面,可以看到 Compiler 和 Compilation 上定义了数十个钩子,每个钩子都有明确的类型(SyncHook、AsyncSeriesHook 等)和触发时机。
Webpack 完整构建流程
初始化阶段
├── 读取配置文件
├── 合并命令行参数
├── 创建 Compiler 实例
├── 注册所有 Plugin(调用 plugin.apply(compiler))
└── 触发 environment / afterEnvironment 钩子
构建阶段
├── 触发 beforeRun / run 钩子
├── 创建 Compilation 实例
├── 触发 compile 钩子
├── 触发 make 钩子
│ └── 从 Entry 出发,递归解析模块
│ ├── 调用 Loader 处理各类文件
│ ├── 使用 acorn 解析 JS 为 AST
│ ├── 遍历 AST 收集依赖
│ └── 递归处理依赖模块
└── 触发 finishMake 钩子
封装阶段
├── 触发 seal 钩子
├── 代码分割(Code Splitting)
├── Tree Shaking(标记未使用导出)
├── Scope Hoisting(作用域提升)
├── 生成 Chunk
└── 触发 afterOptimizeAssets 钩子
输出阶段
├── 触发 shouldEmit 钩子
├── 触发 emit 钩子
├── 生成最终代码(createChunkAssets)
├── 写入文件系统
├── 触发 afterEmit 钩子
└── 触发 done 钩子
text
Loader 的执行流程
Loader 的执行发生在构建阶段的模块解析过程中。
执行时机
当 Webpack 解析到一个模块时:
1. 根据 module.rules 中的 test 匹配文件
2. 确定该文件需要使用的 Loader 列表
3. 从右往左依次执行 Loader
4. 最终产出 JavaScript 代码
text
Loader 执行顺序详解
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 第3步执行:将 CSS 注入 DOM
'css-loader', // 第2步执行:解析 CSS 中的 import/url
'sass-loader' // 第1步执行:SCSS → CSS
]
}
]
}
}
javascript
执行顺序:sass-loader → css-loader → style-loader
数据流向:
源文件 (.scss)
→ sass-loader → CSS 字符串
→ css-loader → JS 模块(CSS in JS)
→ style-loader → 将 CSS 插入 DOM 的 <style> 标签
text
Loader 的分类
| 类型 | 作用 | 示例 |
|---|---|---|
| pre loader | 前置处理(lint、代码检查) | eslint-loader |
| normal loader | 常规转换(编译、转译) | babel-loader, ts-loader |
| inline loader | 内联指定(代码中直接使用) | import Styles from 'style-loader!css-loader?modules!./styles.css' |
| post loader | 后置处理(代码覆盖率等) | istanbul-instrumenter-loader |
执行顺序:pre → normal → inline → post
Plugin 的执行流程
Plugin 通过监听 tapable 钩子来介入构建流程。Plugin 在初始化阶段被注册,在后续的各个构建阶段被触发。
Plugin 注册
class MyPlugin {
apply(compiler) {
// apply 方法在初始化阶段被调用
// 此时可以注册各种钩子
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
compilation.hooks.optimizeChunks.tap('MyPlugin', (chunks) => {
// 在 chunk 优化阶段执行
});
});
}
}
javascript
Plugin 触发时机
以 HtmlWebpackPlugin 为例:
class HtmlWebpackPlugin {
apply(compiler) {
// 监听 emit 钩子(输出阶段)
compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
// 生成 HTML 文件
// 将打包产物的 script 标签注入 HTML
const html = this.generateHTML(compilation);
compilation.assets['index.html'] = {
source: () => html,
size: () => html.length
};
callback();
});
}
}
javascript
Plugin 可介入的阶段
| 阶段 | 钩子 | 典型用途 |
|---|---|---|
| 初始化 | environment | 读取环境变量 |
| 编译前 | beforeRun | 检查配置合法性 |
| 编译中 | compilation | 注册 Compilation 钩子 |
| 模块构建 | make | 添加自定义入口 |
| 优化 | optimizeChunks | 自定义代码分割 |
| 输出 | emit | 修改输出内容 |
| 完成 | done | 输出构建统计 |
Loader 与 Plugin 的协作
一个完整的 Webpack 构建流程中,Loader 和 Plugin 各司其职又相互配合:
Plugin 注册钩子
→ Webpack 开始构建
→ 遇到模块文件
→ Loader 处理文件内容
→ 产出 JS 代码
→ 所有模块构建完毕
→ Plugin 的 seal 阶段钩子触发
→ Plugin 的 emit 阶段钩子触发
→ 最终输出
text
Loader 负责"翻译"——把各种类型的文件转换为 Webpack 可理解的 JavaScript 模块。Plugin 负责"编排"——在整个构建流程的关键节点执行额外逻辑,如代码优化、资源管理、环境变量注入等。
参考资源
- Webpack Compiler Hooks - 完整钩子列表
- Webpack Compilation Hooks - 编译钩子
- Webpack Loader API - Loader 开发文档
↑