loader 是模块内容转换器,仅在单个模块解析前将源码转为 js 模块;plugin 是编译生命周期监听者,通过 tapable 钩子干预 compiler 或 compilation 全流程。

Loader 和 Plugin 看似都在“打包时干活”,但它们介入的层级、执行阶段和依赖的底层机制完全不同——不是“先后顺序”的差别,而是“工作面”和“控制权”的根本差异。
Loader 运行在模块解析阶段,本质是内容转换流水线
Loader 只在 单个模块加载和构建前 被调用,属于 module 层面的处理单元。它不关心整个项目结构、资源产出或编译状态,只专注一件事:把输入的原始文件内容(字符串或 Buffer)变成 Webpack 能识别的 JS 模块代码。
- 执行时机严格限定在
compilation.addModule流程中,即模块被创建、源码读取之后,AST 解析之前 - 底层由
loader-runner库驱动,按use数组从右到左(或从下到上)串行调用每个 loader 函数 - 没有直接访问
compiler或compilation实例的权限,也不能修改构建生命周期钩子 - 同步 loader 直接 return 字符串;异步 loader 必须调用
this.async()并返回 callback 或 Promise
Plugin 运行在整个编译生命周期,本质是事件监听与流程干预
Plugin 是 Webpack 构建系统的“操作系统级扩展”,它通过 Tapable 的钩子系统,在 compiler(全局)或 compilation(单次构建)对象上注册监听器,对整个流程施加影响。
-
compiler.hooks钩子(如run、done、emit)在每次构建启动/结束时触发,Plugin 可在此清理目录、注入环境变量、生成报告 -
compilation.hooks钩子(如buildModule、seal、optimizeChunks、emit)在模块图构建、优化、资源生成等关键节点触发,Plugin 可在此修改模块、拆分 chunk、重写 asset 内容 - 一个 Plugin 的
apply方法必须显式调用compiler.hooks.xxx.tap或compilation.hooks.xxx.tap才能生效 - 可执行异步操作(如调用 fs.writeFile、发起 HTTP 请求),并需正确使用
tapAsync或tapPromise
关键对比:同一构建过程中的角色分工
以一次普通构建为例:
- 当 Webpack 读取
index.js并发现import './style.scss'时,会为style.scss启动模块创建流程 → 此刻触发compilation.hooks.buildModule(Plugin 可监听) - 接着按
rules匹配,依次调用sass-loader→css-loader→style-loader(Loader 流水线运行,无钩子参与) - 所有模块解析完毕、依赖图构建完成,进入
seal()阶段 → 触发compilation.hooks.seal(Plugin 可在此做 Tree Shaking 或 chunk 分析) - assets 生成前,触发
compilation.hooks.emit→mini-css-extract-plugin在此处把 CSS 从 JS 中抽离,html-webpack-plugin在此处生成 HTML 并注入 script 标签 - 最终写入磁盘前,触发
compiler.hooks.emit→clean-webpack-plugin可在此清空输出目录
一句话总结本质区别
Loader 是被调用的函数,运行在模块内容层面,不感知生命周期;Plugin 是主动监听者,运行在编译器对象层面,靠钩子驱动,掌控全流程节奏。











