跳到主要内容

Webpack 的 Loader 和 Plugin 有什么区别?

很多同学学 Webpack 时最容易卡在一个问题:Loader 和 Plugin 到底有什么区别?
你可以先用一句话把它们分开:

  • Loader:把“某个文件”翻译/转换成 Webpack 能继续处理的模块(更像“翻译官”)。
  • Plugin:在 Webpack 的“构建生命周期”里插入能力,改变构建流程/优化/产物(更像“流水线总控/工头”)。

下面我们用工作位置、职责边界、写法、典型场景把它讲透。


一、从“负责什么”开始:它们的职责边界

对比维度LoaderPlugin
处理对象单个模块/文件(命中 module.rules 的资源)整个构建过程(Compiler/Compilation 生命周期)
主要作用转换源码(把非 JS 或新语法变成可打包的模块)扩展/改造构建行为(优化、注入、产物加工、报告等)
介入时机模块构建阶段(解析依赖前后)全流程(初始化、构建、优化、输出等)
配置位置module.rules[].useplugins: [new XxxPlugin()]
典型产出返回一段 JS(或可被后续 Loader/Parser 处理的代码)修改 compilation、生成/修改 asset、影响 chunk、注入运行时代码等

直觉判断

  • 你的需求是否“针对某类文件怎么被读/怎么被转”?——优先想 Loader
  • 你的需求是否“影响打包流程、产物结构、优化策略、注入全局变量/脚本”?——优先想 Plugin

二、Loader 是什么:它解决“怎么把这个文件变成模块”

2.1 Loader 的典型工作:把“不能直接打包的东西”变成“模块”

Webpack 默认只认识 JS/JSON(以及少量内置能力)。当你引入这些资源时:

  • import './index.css'
  • import logo from './logo.png'
  • import App from './App.tsx'

都需要一条“翻译链”把它们变成 Webpack 能继续处理的模块。

常见 Loader 例子(记住“它们都在做转换”):

  • babel-loader:把新语法/JSX 转成目标环境可运行的 JS
  • ts-loader / swc-loader:把 TS/TSX 转成 JS
  • css-loader:把 CSS 变成 JS 模块(导出 className 等)
  • style-loader:把 CSS 以 <style> 的形式注入到页面运行时
  • sass-loader / less-loader:把 Sass/Less 编译成 CSS

2.2 Loader 链执行顺序:从右到左(从后到前)

当你写:

module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
};

实际执行顺序是:

  1. postcss-loader(先把 CSS 做兼容/压缩等处理)
  2. css-loader(把 CSS 变成 JS 模块)
  3. style-loader(把 CSS 注入页面)

你可以把它理解为“管道”:

2.3 一个最小 Loader 示例:给源码加一段 banner

目标:在每个命中的模块顶部加一句注释,帮助调试。

loaders/banner-loader.js

module.exports = function bannerLoader(source) {
const options = this.getOptions ? this.getOptions() : {};
const banner = options.banner ?? '/* built by banner-loader */';
return `${banner}\n${source}`;
};

使用方式:

// webpack.config.js(片段)
const path = require('path');

module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, 'loaders/banner-loader.js'),
options: { banner: '/* hello loader */' },
},
],
},
],
},
};

你会发现:Loader 的输入/输出都很“局部”——它只关心这个模块的源码怎么变。

2.4 Loader 里常见的“工程化细节”(面试也爱问)

  • 同步 vs 异步:同步直接 return,异步用 this.async() 拿到回调。
  • 缓存this.cacheable(true) 表示结果可缓存(纯函数式转换更适合)。
  • raw:若 module.exports.raw = true,Loader 会接收 Buffer
  • pitch:高级用法,允许在正常链执行前“拦截”(了解即可)。

三、Plugin 是什么:它解决“我想改变整个构建过程”

3.1 Plugin 的工作方式:在 Hook 上“插一个回调”

Webpack 内部把构建过程拆成很多阶段,并暴露成 Hook(钩子)。Plugin 的核心就是:

  1. 实现一个带 apply(compiler) 的类
  2. apply 里订阅某些 Hook
  3. 在 Hook 回调里读/改构建数据结构,甚至生成新产物

常见 Plugin 例子(记住“它们都在改流程/产物”):

  • HtmlWebpackPlugin:根据模板生成 HTML,并把打包出来的资源注入进去
  • DefinePlugin:在编译期做常量替换(比如注入 process.env.NODE_ENV
  • MiniCssExtractPlugin:把 CSS 从 JS 中抽离成独立 .css 文件(它通常同时配合一个 loader
  • CopyWebpackPlugin:拷贝静态资源到输出目录

3.2 一个最小 Plugin 示例:额外生成一个 build-info.txt

目标:每次构建时,在 dist/ 里额外输出一个文件,写入构建时间。

plugins/build-info-plugin.js

class BuildInfoPlugin {
apply(compiler) {
const { Compilation, sources } = compiler.webpack;

compiler.hooks.thisCompilation.tap('BuildInfoPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'BuildInfoPlugin',
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
() => {
const content = `builtAt=${new Date().toISOString()}\n`;
compilation.emitAsset('build-info.txt', new sources.RawSource(content));
},
);
});
}
}

module.exports = BuildInfoPlugin;

使用方式:

// webpack.config.js(片段)
const BuildInfoPlugin = require('./plugins/build-info-plugin');

module.exports = {
plugins: [new BuildInfoPlugin()],
};

你会发现:Plugin 的视角是“全局”的——它并不是在改某个模块的源码,而是在改 compilation 产物

3.3 Plugin 里常见的两个对象:compilercompilation

  • compiler:一次构建的“总指挥”,贯穿从启动到结束的生命周期(dev-server 下可能会多次触发构建)。
  • compilation:单次编译的“现场数据”,里面有 module、chunk、asset 等构建中间产物。

面试时如果你能说清楚这两个对象的定位,基本就稳一半。


四、为什么有时必须“Loader + Plugin 组合”?

最经典的例子是 抽离 CSS

  • css-loader 负责把 CSS 变成模块(Loader 的职责)
  • MiniCssExtractPlugin.loader 负责把这些 CSS 模块从 JS 里“收集”出来(Loader 负责把信息挂到模块上)
  • MiniCssExtractPlugin 负责在生成资产阶段把 CSS 写成独立文件(Plugin 的职责)

所以你经常会看到这种配置:

{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
}

记住这个结论:Loader 更像“改原材料”,Plugin 更像“改流水线和产物”。很多复杂能力需要两者配合。


五、常见误区与最佳实践

  1. 把 Plugin 当成 Loader 用:想改某类文件的内容却去写 Plugin,通常会更复杂、更难维护。优先用 Loader。
  2. Loader 里做“全局副作用”:Loader 会对很多模块重复执行,做 IO/写文件/改全局变量容易引发性能与并发问题。
  3. Webpack 5 资产阶段推荐用 processAssets:比老的 emit/afterEmit 更清晰,也更容易控制阶段顺序。
  4. 能复用就别硬造轮子:大多数需求已有成熟 loader/plugin;自定义更多是为了理解原理或满足强定制。

六、面试高频问答

1)一句话区分 Loader 和 Plugin?

  • Loader 负责“把文件变成模块”;Plugin 负责“改变整个构建过程和产物”。

2)Loader 为什么是从右到左执行?

  • 因为配置里 use 更像“管道拼接”:越靠右越接近原始输入,先把原材料处理好,再交给左边的 loader 做后续加工。

3)style-loader 是 Loader 还是 Plugin?它在做什么?

  • 是 Loader。它把 css-loader 产出的 CSS 内容包装成运行时代码,最终在浏览器里把样式插入到 <style> 标签中。

4)为什么 MiniCssExtractPlugin 既有 Plugin 又有一个 loader?

  • 因为它需要“模块级采集 + 全局产出”:loader 在模块构建时记录/提取信息,plugin 在资产生成阶段把 CSS 统一输出成文件。

5)什么需求更适合写 Loader?

  • 文件级转换:TS/JSX、样式预处理、把某类文本转成 JS 导出、对源码做 AST 转换等。

6)什么需求更适合写 Plugin?

  • 构建级能力:生成/修改输出文件、注入全局常量、分析构建产物、控制拆包策略、在特定阶段中止/报错等。

7)Plugin 是怎么“插进” Webpack 的?

  • Webpack 把各阶段暴露为 Hook(Tapable),Plugin 在 apply(compiler) 中通过 compiler.hooks.xxx.tap(...) 订阅钩子。

8)compilercompilation 的区别?

  • compiler 是一次启动的全局构建控制器;compilation 是一次具体编译的上下文数据(模块、chunk、asset 等都在这里)。

9)Loader 里如何读取配置项(options)?

  • Webpack 5 推荐用 this.getOptions(),不再依赖老的 loader-utils

10)如果我想“构建完成后输出一个报告文件”,用 Loader 还是 Plugin?

  • 用 Plugin。因为这是对整体产物的处理,不是某个模块的源码转换。