想象你是一家预制菜中央厨房的负责人。后厨每天会收到一堆“原生食材”:带泥的萝卜、整扇的牛肉、还没泡发的干香菇。但掌勺大厨只认“净菜”——已经洗好、切好、能直接下锅的半成品。Webpack 就像这座中央厨房的总管道,它本身只认识 JavaScript 这一种“净菜”。SCSS、TypeScript、图片、字体这些食材,都必须先被处理成它认识的形态,才能进入打包流程。 在这个比喻里,Loader 就是一台台食材加工机:把 SCSS 洗成 CSS、把 TypeScript 切成 JavaScript、把小图片榨成 Base64 字符串。而 Plugin 则是厨房里的产线调度中心:它决定什么时候开火、什么时候关烤箱、什么时候把成品装盒扫码出库。一个管“怎么把原材料变成半成品”,一个管“整个构建流程还能增加哪些环节”。这篇文章会带你从零看懂这两套机制,并亲手写一个自定义 Loader 和 Plugin。

岗位关联

对 AI 从业者来说,掌握 Webpack 的扩展机制意味着你能随心定制前端构建流程:从加载模型配置文件、注入版本水印,到优化 AI 平台前端产物,都能自己动手实现,不再被现成脚手架困住。

目录
  1. 可视化演示
  2. Loader 核心概念
  3. 常用 Loader 配置
  4. Loader 执行机制
  5. Plugin 核心概念
  6. 常用 Plugin 用法
  7. 自定义 Loader 实战
  8. 自定义 Plugin 实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Webpack Loader链式调用文件转换、Plugin钩子注入构建生命周期、Compiler与Compilation对象关系。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Loader链式调用与执行顺序 SCSS源文件经sass-loader→css-loader→style-loader链式转换 链从右到左执行,输出JS模块 output = style(css(sass(input))) 每个Loader接收source返回转换后内容

Loader 核心概念

一筐带泥的萝卜进入中央厨房,大厨不会直接拿它下锅——它得先被冲洗、削皮、切块。Webpack 也一样,它只把 JavaScript 当作“净菜”,其他格式的文件统统需要先加工。这个加工动作的承担者就是 Loader。

Loader 本质上是一个 Node 模块导出的函数。它接收源文件的文本内容,经过加工后把新内容返回给 Webpack。在配置里,我们用 module.rules 来告诉 Webpack:哪些文件需要加工、用哪台机器加工。test 字段像一个食材分拣标签,凡是文件名能匹配正则的,就会被送进对应的产线。

Loader 最特别的地方是它可以链式处理。就像萝卜要经过“清洗 → 切丝 → 榨汁”三台机器,前一台机器的输出会直接成为后一台机器的输入。用公式表示就是:

回到中央厨房:没有谁要求一台机器干完所有活。把工序拆细,每台机器专精一件事,需要换菜谱时只要调整产线上的机器顺序就好。这就是 Loader 被设计成可链式组合的原因。

result=L3(L2(L1(source)))\displaystyle \text{result} = L_3\left(L_2\left(L_1(\text{source})\right)\right)
通俗地说:文件先被第一个 Loader 处理,处理结果交给第二个 Loader,再交给第三个,最终产物才是 Webpack 能识别的模块。
  1. source(原始文件)这是尚未加工的文件内容,比如一段 SCSS 源码。
    source\text{source}
  2. L1 第一次加工第一个 Loader 接收 source,完成自己的转换任务,比如把 SCSS 编译成 CSS。
    L1(source)L_1(\text{source})
  3. L2 第二次加工第一个 Loader 的输出会成为第二个 Loader 的输入,比如进一步解析 CSS 里的 url() 依赖。
    L2(L1(source))L_2\left(L_1(\text{source})\right)
  4. result(最终产物)最后一个 Loader 输出的才是 Webpack 真正认识的 JavaScript 模块。
    L3(L2(L1(source)))L_3\left(L_2\left(L_1(\text{source})\right)\right)

常用 Loader 配置

知道了 Loader 是加工机,我们得给厨房配齐常用设备。不同的食材对应不同的机器,前端项目里最常用的 Loader 配置有下面这些。

Babel 编译 JavaScript:现代前端代码大量使用 ES6+ 语法,但部分浏览器还只认识 ES5。babel-loader 可以把 JSX、箭头函数、可选链等语法转换成兼容性更好的代码。它通常带着 @babel/preset-env 一起工作,就像设置好菜谱的自动炒菜机。

样式处理链:处理 SCSS 时,最常见的配置是三台机器串联:

这里出现了“看起来从左到右,实际上从右到左”的微妙规则。sass-loader 先把 SCSS 变成 CSS,css-loader 再解析 CSS 里的 import 和 url(),最后 style-loader 把样式以 <style> 标签的形式注入页面。

Sass 与 PostCSS:postcss-loader 常跟在 sass-loader 后面,为 CSS 自动添加浏览器前缀,相当于给半成品贴统一的合规标签。配合 autoprefixer 插件,可以省去手写 -webkit-、-moz- 前缀的麻烦。

文件资源处理:图片、字体这类二进制资源,可以用 file-loader 把它们复制到输出目录并返回访问路径;如果文件很小,url-loader 还会直接把内容编码成 Base64 内联到 JS 里,减少 HTTP 请求次数。

这些配置看似繁琐,但思路非常一致:每一种文件格式,都对应一条明确标注了加工工序的产线。你只需要把机器按正确的顺序接好,剩下的交给 Webpack 的流水线自动运行。

js4 行
{  test: /\.scss$/,  use: ['style-loader', 'css-loader', 'sass-loader']}

Loader 执行机制

配置 Loader 时最容易踩的坑就是执行顺序。中央厨房的传送带有时会让人困惑:控制台上写着“先放洗菜机、再放切菜机”,可实际运转时却是切菜机先启动、洗菜机后启动。

Webpack 的 use 数组正是这种“反直觉”的设计。我们写:

表面顺序是 sass → css → style,但真正执行时是从右往左:style 最先被调用预处理,然后 css,最后 sass。加工完成后再把结果从右往左回传:

除了逆序规则,还可以通过 enforce 字段手动调整 Loader 的阶段。enforce: 'pre' 表示在正常 Loader 之前执行,enforce: 'post' 表示在之后执行,适合做代码检查这类“提前拦截”或“最终兜底”的工作。

Loader 还有一个隐藏技能叫 pitching。loader 可以导出一个 pitch 函数,它在所有正常 loader 执行之前从左到右运行。如果某个 pitch 返回了非 undefined 的值,后续的 loader 会被全部跳过——就像质检员发现萝卜已经坏了,直接叫停整条产线,不用再浪费后面的工序。loader-utils 则是官方提供的工具包,里面的 getOptions(this) 专门用来读取 loader 配置项,是手写自定义 Loader 的常用帮手。

result=styleLoader(cssLoader(sassLoader(source)))\displaystyle \text{result} = \text{styleLoader}\left(\text{cssLoader}\left(\text{sassLoader}(\text{source})\right)\right)
记住一句话:use 数组像一份“倒放的工序单”,最后一个 Loader 才是第一个真正吃到原始文件的。
  1. use 数组从左到右声明配置时我们按“用户直觉”顺序写下 sass-loader、css-loader、style-loader。
    use=[sass,css,style]\text{use} = [\text{sass}, \text{css}, \text{style}]
  2. 实际从右到左执行Webpack 从数组末尾开始,sass-loader 第一个读取原始 SCSS 文件。
    sassLoader(source)\text{sassLoader}(\text{source})
  3. 前一个输出作为后一个输入CSS 结果传给 css-loader,再传给 style-loader。
    cssLoader(sassLoader(source))\text{cssLoader}\left(\text{sassLoader}(\text{source})\right)
  4. 最终结果返回 Webpack最后一个执行完的 style-loader 输出才是最终产物。
    styleLoader(cssLoader(sassLoader(source)))\text{styleLoader}\left(\text{cssLoader}\left(\text{sassLoader}(\text{source})\right)\right)
js1 行
use: ['sass-loader', 'css-loader', 'style-loader']

Plugin 核心概念

如果说 Loader 是负责“单点加工”的机器,Plugin 就是整个中央厨房的“调度系统”。它不直接参与某一盘菜的制作,但可以决定何时开门、何时关气、何时在餐盒上贴标签。

Plugin 在 Webpack 里是一个带有 apply 方法的 JavaScript 类。当 Webpack 启动时,会调用每个 Plugin 的 apply(compiler),把最重要的两个对象交到它手上:Compiler 和 Compilation。

Compiler 是整个构建过程的全局对象。它代表从启动到结束的完整生命周期,相当于厨房总经理。Compilation 则代表一次具体的构建任务,里面装着当前这一轮构建的所有模块、依赖、资源,相当于某一天的具体出餐订单。如果项目监听文件变化触发了多次构建,Compiler 只有一个,Compilation 会有多个。

Plugin 通过“钩子”来嵌入流程。Webpack 在特定时机触发钩子,Plugin 用 tap 方法注册自己的回调。同步任务用 tap,异步任务用 tapAsync 或 tapPromise。这就像厨房里装了公告喇叭:“准备开火时广播一声,想听的人自己来报名。” Plugin 不需要侵入 Webpack 源码,只需在合适的时间点做自己的事。

这种设计让构建流程变得高度可扩展:想要清理旧文件?挂一个回调。想要生成 HTML?再挂一个回调。每个 Plugin 都是“订阅事件”的独立模块,互不干扰,组合起来却能完成极其复杂的构建需求。

常用 Plugin 用法

理解了“调度系统”的思路,我们再来看厨房里最常见的几个“自动化工位”。

HtmlWebpackPlugin 是几乎所有项目都会用到的插件。它根据模板生成 HTML 文件,并自动把所有打包出来的 JS/CSS 文件以 <script> 或 <link> 标签的形式注入进去。以前我们需要手写 HTML 引入路径,现在它像一位自动摆盘的员工,把菜品和餐具一次性放好。

CleanWebpackPlugin 负责“每顿开饭前先清桌”。每次构建前它会删除输出目录中的旧文件,防止上次构建残留的垃圾文件混进新产物。没有它,dist 目录会越积越乱,就像厨房灶台一周不擦。

MiniCssExtractPlugin 把 CSS 从 JS 中抽离出来,生成独立的 .css 文件。它通常配合生产模式使用,比 style-loader 的运行时注入更适合线上环境。使用它时,需要把规则里的 style-loader 替换成 MiniCssExtractPlugin.loader,并同时把插件实例加入 plugins 数组。

DefinePlugin 则像一个“食材标签机”,在编译时把代码里的 process.env.NODE_ENV 等变量直接替换成具体值。注意它并不是在运行时设置环境变量,而是把源码中的标识符替换成字符串字面量,从而让打包工具在压缩时剔除无效分支。

这些 Plugin 的用法都遵循同样的模式:new PluginClassName(options),然后放进 plugins 数组。它们解决的是不同环节的自动化问题,合理组合起来,整个构建流程几乎不需要人工干预。

自定义 Loader 实战

当你需要处理一种 Webpack 不认识的自定义文件格式时,就该自己造一台加工机了。手写 Loader 并不神秘,它就是一个普通函数:

这个 Loader 会把源码中的 /* ... */ 注释全部删掉。source 是 Webpack 传入的原始文件内容,返回值就是处理后的结果。函数里的 this 是 Webpack 注入的上下文对象,提供了许多实用方法。

如果希望同时返回 sourceMap 或更多信息,可以用 this.callback:

this.callback(null, code, map, meta) 可以一次性把转换后的代码、sourceMap、元数据都交给 Webpack,比直接 return 更灵活。

遇到需要异步操作的场景,比如读取文件、请求网络,就用 this.async():

this.async() 会返回一个回调函数,等异步任务完成后再把结果传回 Webpack。对于二进制文件,比如图片或自定义模型文件,需要设置 module.exports.raw = true;,告诉 Webpack 传入 Buffer 而不是字符串。

亲手写一个 Loader,能让你彻底理解“加工机”的含义:输入什么、输出什么、怎样配置选项、怎样回传结果。一旦掌握了这套函数式转换思路,再奇怪的文件格式都能被接进构建流水线。

js5 行
// strip-comment-loader.jsmodule.exports = function (source) {  const options = require('loader-utils').getOptions(this);  return source.replace(/\/\*[\s\S]*?\*\//g, '');};
js4 行
module.exports = function (source) {  const newSource = source.replace(/TODO/g, 'done');  this.callback(null, newSource, null);};
js4 行
module.exports = function (source) {  const callback = this.async();  setTimeout(() => callback(null, source.toUpperCase()), 100);};

自定义 Plugin 实战

自定义 Plugin 比自定义 Loader 更像“在产线上加一个自动工位”。它不能直接改文件内容,但可以在关键时间点插一脚,甚至往构建产物里塞新文件。

一个最基本的自定义 Plugin 长这样:

这个插件注册到 emit 钩子上。emit 是 Webpack 在准备输出资源前触发的钩子,此时所有模块都已编译完毕,产物清单存在 compilation.assets 里。我们往里面添加一个 version.json 文件,等构建完成后它就会出现在 dist 目录中。

compilation.assets 里的每个资源对象必须提供两个方法:source() 返回文件内容,size() 返回文件大小。这就是 Webpack 认识“文件”的方式。Plugin 的本质就是通过钩子获得操作这些内部对象的机会。

实战中,这类插件很有价值。比如自动生成版本号、打包时间、Git commit hash,或者扫描构建产物并生成资源清单。再比如配合 HtmlWebpackPlugin,在 HTML 注入 <meta> 标签或统计脚本。只要理解了 apply、compiler.hooks、compilation.assets 这三个核心概念,你就能按需定制任何构建行为。

js14 行
class VersionPlugin {  apply(compiler) {    compiler.hooks.emit.tapAsync('VersionPlugin', (compilation, cb) => {      const version = new Date().toISOString();      compilation.assets['version.json'] = {        source: () => JSON.stringify({ version }),        size: () => JSON.stringify({ version }).length      };      cb();    });  }} module.exports = VersionPlugin;

AI 应用场景

自定义 Loader 加载 AI 平台的自定义数据文件

阶段:前端工程化与模型产物集成

解决的问题:AI 平台经常需要在前端展示实验配置、模型指标、样本标注等非标准格式文件。传统方式要么手动转成 JSON,要么让后端二次解析,繁琐且容易出错。自定义 Loader 可以直接把 .metrics 或 .config 文件转换成 JS 模块,在构建时自动完成解析和校验。

具体案例:一个模型训练平台希望前端直接 import config from './experiment.metrics'。团队写了一个 metrics-loader,读取自定义文本格式,用正则解析出 loss、accuracy 等字段,并统一转换成 JSON 对象。从此新增实验指标格式时,只需修改 Loader 规则,所有页面自动生效。

自定义 Plugin 自动生成构建版本水印

阶段:模型服务前端发布与版本回溯

解决的问题:AI 产品上线后,经常需要快速定位某个页面对应哪个模型版本、哪次代码提交。手写版本文件容易遗漏。自定义 Plugin 可以在每次构建时自动读取 Git commit hash 和当前时间,写入 version.json,并随构建产物一起发布。

具体案例:团队写了一个 VersionInfoPlugin,监听 emit 钩子,把 git rev-parse HEAD 的结果注入 compilation.assets['version.json']。前端在页面加载时请求这个文件,展示“当前版本:a3f9c2e”。出现问题后,运维能立刻对应到具体代码和模型参数。

Python 演示

python58 行
class MiniBuild:    def __init__(self, source):        self.source = source        self.loaders = []        self.plugins = []        self.log = []     def use(self, loader_func):        self.loaders.append(loader_func)     def add_plugin(self, plugin):        self.plugins.append(plugin)     def run_plugins(self, stage):        for plugin in self.plugins:            plugin.run(stage, self.log)     def build(self):        self.run_plugins('before')        result = self.source        for loader in reversed(self.loaders):            result = loader(result)        self.result = result        self.run_plugins('after')        return self.result  def sass_loader(source):    return source.replace('$primary', '#818CF8')  def css_loader(source):    return '/* css */ ' + source  def style_loader(source):    return 'module.exports = ' + repr(source)  class BannerPlugin:    def __init__(self, text):        self.text = text     def run(self, stage, log):        if stage == 'after':            log.append('BannerPlugin: ' + self.text)  if __name__ == '__main__':    build = MiniBuild('$primary { color: red }')    build.use(sass_loader)    build.use(css_loader)    build.use(style_loader)    build.add_plugin(BannerPlugin('built by MiniBuild'))    output = build.build()    print(output)    for line in build.log:        print(line)
目的:用 70 行 Python 模拟 Webpack 的 Loader 链与 Plugin 钩子,帮助你直观理解执行顺序和扩展机制。
  1. self.loaders.append(loader_func)把一个个加工机放进流水线,对应 Webpack 的 use 数组。
  2. for loader in reversed(self.loaders):核心中的核心:从右向左执行 Loader。
  3. result = loader(result)上一个 Loader 的输出,成为下一个 Loader 的输入。
  4. plugin.run(stage, self.log)模拟 Plugin 在构建前和构建后挂载回调。
  5. build.add_plugin(BannerPlugin('built by MiniBuild'))注册一个“构建完成后打水印”的 Plugin。

常见误区

误区`use: ['a-loader', 'b-loader']` 会先执行 a-loader,再执行 b-loader。因为数组从左到右读,处理顺序肯定也是从左到右。

正解Webpack 的 use 数组实际执行顺序是从右向左。b-loader 先拿到原始文件,处理完交给 a-loader。这就像剥洋葱,配置里写在最右边的机器最先接触最外层的皮。

为何常见:几乎所有编程场景里数组都是按索引顺序读取,所以人们默认执行顺序和书写顺序一致。

误区Plugin 和 Loader 都能处理文件内容,区别只是写法不同,Plugin 也能像 Loader 一样直接转换源码。

正解Loader 专注于“把文件转成 JS 模块”,操作对象是单个文件的源码;Plugin 操作的是整个构建流程和构建对象,比如 Compiler、Compilation、assets。Plugin 虽然可以修改 compilation.assets,但这不是它的主要职责。

为何常见:两者都能影响最终产物,初学者容易从“结果相似”推导出“本质相同”。

误区想定制构建流程,就得去改 Webpack 内部代码,或者把 Webpack 源码复制一份来改。

正解Plugin 的扩展点是官方预先留好的钩子。在 apply(compiler) 里调用 compiler.hooks.xxx.tap(),就能在对应时机执行自己的代码。这相当于在厨房公布栏上“报名值班”,不需要拆掉厨房的墙。

为何常见:以前用过的一些构建工具扩展成本高,让人觉得所有工具都只能靠改源码来定制。

误区在 Loader 里使用 `async/await` 直接返回结果,Webpack 就会自动等待异步操作完成。

正解Loader 是同步执行模型,如果需要异步处理,必须调用 this.async() 获取回调,等异步任务完成后把结果传给回调。不能简单地把 Loader 函数标记为 async,否则 Webpack 拿到的会是一个 Promise。

为何常见:现代 JS 开发中 async/await 太普及,容易忽略工具框架自身的执行模型约束。

面试题

Q1. Loader 在 Webpack 中的本质是什么?

单选题 · easy

  • A.一种打包压缩 JS 的插件
  • B.导出函数的转换模块← 正确答案
  • C.管理 Webpack 配置的入口
  • D.运行时修改浏览器 DOM 的库

答案:B。Loader 是一个导出函数的 Node 模块,它接收源文件内容并返回转换后的内容。插件是另一个概念,配置入口也不是 Loader 的职责。

Q2. 配置 use: ['sass-loader', 'css-loader', 'style-loader'] 时,正常执行顺序是?

单选题 · easy

  • A.从左到右依次执行
  • B.从右到左依次执行← 正确答案
  • C.按文件大小自动排序
  • D.按 Loader 名称字母排序

答案:B。Webpack 的 use 数组从右向左执行。style-loader 最先被调用,最后执行的是 sass-loader,它第一个接触原始 SCSS 文件。

Q3. 下列哪些是 Loader 可以完成的常见任务?

多选题 · easy

  • A.把 SCSS 转成 CSS← 正确答案
  • B.把 ES6 转成 ES5← 正确答案
  • C.监听端口启动开发服务器
  • D.把小图片转成 Base64← 正确答案

答案:ABD。启动开发服务器是 devServer 或相关 Plugin 的职责,Loader 不负责网络服务。SCSS 编译、Babel 转换、图片内联都是典型 Loader 应用场景。

Q4. 自定义 Plugin 时必须实现哪个方法?

单选题 · mid

  • A.apply← 正确答案
  • B.init
  • C.run
  • D.compile

答案:A。Plugin 的核心是 apply(compiler) 方法。Webpack 启动时会调用它,并把全局的 Compiler 对象交给 Plugin,让它去注册钩子。

Q5. 关于 Compiler 与 Compilation,下列说法正确的是?

单选题 · mid

  • A.Compiler 只负责压缩,Compilation 负责转换
  • B.Compiler 是全局构建对象,Compilation 是本次构建状态← 正确答案
  • C.二者完全相同
  • D.Compilation 贯穿整个生命周期,Compiler 只存在一次

答案:B。Compiler 代表从启动到结束的完整构建生命周期,只有一个;Compilation 代表某一次具体构建的模块、依赖和资源状态,可以多次出现。

Q6. 在自定义 Loader 中,this.callback 的主要作用是什么?

单选题 · mid

  • A.向 Webpack 回传代码、sourceMap 等多个值← 正确答案
  • B.抛出构建错误并终止打包
  • C.向远程服务器发送请求
  • D.修改其他 Loader 的配置

答案:A。this.callback(null, code, map, meta) 可以一次性传递转换后的代码和辅助信息。抛出错误应该用 this.emitError,发送请求不是它的职责。

Q7. 关于 Loader 的 pitch 函数,下列说法正确的是?

多选题 · mid

  • A.pitch 从左到右执行← 正确答案
  • B.pitch 返回非 undefined 会熔断后续 Loader← 正确答案
  • C.pitch 一定在 normal 之后执行
  • D.pitch 只能用于异步 Loader

答案:AB。pitch 阶段在正常 loader 之前运行,顺序从左到右。如果某个 pitch 返回了非 undefined 值,后续 loader 会被跳过。pitch 与异步无关。

Q8. HtmlWebpackPlugin 的核心作用是什么?

单选题 · hard

  • A.压缩 HTML 文件中的 JS 代码
  • B.根据模板生成 HTML 并自动注入打包产物← 正确答案
  • C.将 HTML 文件转成 JS 模块
  • D.开启浏览器热更新服务

答案:B。HtmlWebpackPlugin 会生成 HTML 文件,并把打包后的 JS/CSS 以标签形式自动注入。它不负责转译模块,更不负责启动 devServer。

Q9. 在自定义 Plugin 的 emit 钩子中,可以完成哪些操作?

多选题 · hard

  • A.往 compilation.assets 中新增文件← 正确答案
  • B.修改输出资源的文件内容← 正确答案
  • C.改变 Loader 的执行顺序
  • D.添加构建版本水印← 正确答案

答案:ABD。emit 钩子触发时,产物清单已经生成,可以新增或修改 compilation.assets 里的资源,比如写出版本文件。Loader 执行顺序在编译早期确定,无法在 emit 阶段改变。

Q10. Loader 需要处理 Buffer 二进制文件时,应该设置哪个属性?

单选题 · extreme

  • A.module.exports.raw = true;← 正确答案
  • B.module.exports.binary = true;
  • C.this.raw = true;
  • D.loader.encoding = 'buffer';

答案:A。在 Loader 导出函数上设置 module.exports.raw = true,Webpack 就会给 loader 传入 Buffer 而不是字符串。其他选项都不是 Webpack 约定的 API。