深入理解 Webpack Loader
2025-06-16 · 10min前言
前面已经啃完了 Webpack 的核心概念了,现在准备深入了解下 loader。
loader 的本质
loader 核心概念 里以及讲过了,loader 的本质就是一个函数,接收一个字符串或者 buffer,返回一个 JS 模块的函数。
特性
- 单一职责原则:一个 loader 只做一件事情。不要想着用一个 loader 就把所有事情处理完了。
- 链式调用:多个 loader 串联时,会按倒序调用,输出结果会传给下一个 loader。
- 同步/异步:loader 可以是同步的(直接返回转换结果),也可以是异步的(需要调用回调函数返回结果)。
- 可接收配置:可以通过 Webpack 配置中的
options对象向 loader 传参。 - 无状态:在转换过程中,loader 不应该保留自身状态,每次转换都应该是独立的。
此外 Webpack 提供了 loader-utils 和 schema-utils 等工具包辅助开发。
loader 执行机制与生命周期
前面已经提到过了,loader 链式调用的执行顺序。倒序调用,上一个 loader 输出结果,会给下一个 loader 接收继续执行。
除此之外还有一些需要知道的点。
loader 函数签名
/**
* @param {string|Buffer} content 源文件的内容
* @param {object} [map] 可选的 SourceMap 数据(来自上一个 loader)
* @param {any} [meta] 可选的元数据
* @returns {string|Buffer|void} 转换后的内容。可以可选地返回 SourceMap 和元数据。
*/
module.exports = function(content, map, meta) {
// 同步处理:直接返回转换结果
// return someSyncTransform(content);
// 异步处理:使用 this.async() 获取回调函数
const callback = this.async();
someAsyncTransform(content, (err, result, sourceMaps, newMeta) => {
if (err) return callback(err);
// 参数:错误, 转换后的内容, 可选的 SourceMap, 可选的元数据
callback(null, result, sourceMaps, newMeta);
});
};
this 上下文
函数签名例子中可以看到,Webpack 为 loader 提供了一个 this 上下文对象,具有以下属性和方法:
this.async:异步 loader 需要调用此函数,通知 Webpack 该 loader 是异步的。同时返回一个callback回调函数,异步 loader 需要通过callback返回结果或错误:// 参数:错误, 转换后的内容, 可选的 SourceMap, 可选的元数据 callback(null, result, sourceMaps, newMeta);this.callback:与this.async()返回的回调函数一样,用于返回结果或错误,如果使用this.async(),就用它返回的callback。this.emitFile: 产生一个输出文件。this.emitFile( name: string, content: Buffer | string, sourceMap: SourceMap )this.resourcePath:当前处理文件的绝对路径。this.resourceQuery:当前处理文件的查询字符串(如?date=2025-06-16)。this.rootContext:Webpack 配置中的context选项。this.getOptions(schema?: JSONSchema):获取 loader 配置中options对象,结合schema-utils对选项进行校验。(Webpack 5 之前该方法需要从loader-utils库中导入)。this.emitWarning(warning: Error):发出警告。this.emitError(error: Error):发出错误。this.loadModule(request: string, callback: function): 加载另一个模块(谨慎使用)。this.addDependency(file: string): 添加一个文件作为依赖,使 Webpack 能够监听其变化并触发重新构建(如less-loader添加导入的.less文件)。this.cacheable(flag: boolean = true): 标记 loader 的结果是否可缓存。默认 true 对性能很重要。this.mode: Webpack 的运行模式 ('production' | 'development' | 'none')。
Pitch 阶段
Loader 执行顺序是倒序执行的,这个阶段也被称为 normal 阶段,但在此之前还存在一个顺序执行的 pitch 阶段,在实际处理文件内容之前发生。
方法签名:
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
// remainingRequest: 当前 loader 之后还需要处理的 loader 和资源路径的字符串
// precedingRequest: 当前 loader 之前已经处理过的 loader 的字符串
// data: 可以在 pitch 和 normal 函数间共享的 data 对象
// 如果返回非 undefined 值,会跳过后面 loader 的 pitch 和 normal 执行,以及文件读取,直接进入前一个 loader 的 normal 阶段!
};
用途
- 提前中断:如果
pitch方法返回了内容,则跳过包括它自己和后面(右边) loader 的pitch和normal方法,以及实际读取文件的操作,直接进入前一个(左边) loader 的normal阶段,并将pitch返回值作为该 loader 的normal函数输入。 - 共享数据:通过
data对象在pitch和normal两个函数之间共享数据。 - 元数据处理/前置检查: 在真正处理文件前做一些操作。
场景举例
style-loader 的职责是将 CSS 动态插入 DOM,如果在 normal 阶段处理,style-loader 是无法直接获取 css-loader 的处理结果的,因为:
- loader 输入是字符串或者 Buffer,输出的 JS 模块,是以字符串形式输出的
export default...,下一个 loader 接收的是这个字符串。 require()会触发二次加载,导致 Webpack 重新解析依赖,造成循环或冗余处理。
通过 pitch 方法,可以按以下步骤解决问题:
- 在
pitch阶段返回一段动态生成的 JS 代码,提前中断后续 loader 执行。 - 在生成的 JS 代码中,通过
require显示指定后需要 loader 链处理目标 CSS 文件:module.exports.pitch = function (remainingRequest) { // 1. 生成内联请求路径(包含后续 loader) const request = `!!${remainingRequest}`; // 示例: "!!css-loader!./style.css" // 2. 返回动态 JS 代码 return ` var content = require(${JSON.stringify(request)}); var style = document.createElement('style'); style.innerHTML = content.toString(); document.head.appendChild(style); `; };!!语法表示跳过全局配置的loader,仅使用内联指定的 loader,避免重复处理。 - 生成的代码在浏览器运行时执行,调用
require()获取css-loader处理后的模块对象,而非字符串,从中提取 CSS 字符串插入 DOM 中。
自定义 Loader 开发
了解完了 Loader 的本质,执行机制与生命周期,其实就可以尝试开发一款自己的自定义 loader了。
正当我没什么灵感的时候突然想到,我一直很喜欢 Unocss 里读取指定目录下 *.svg 文件,自动转成生成图标 CSS 类的操作,这不正是 loader 核心职责吗!
开发
依赖安装
pnpm add -save -D style-loader css-loader
loader 开发
在根目录中新建文件 svg-icon-loader.js
const path = require("path");
// 定义 loader 选项的 JSON Schema
const schema = {
type: "object",
properties: {
prefix: {
type: "string",
default: "icon-",
},
},
additionalProperties: false,
};
/**
* @description SVG 转 CSS 类
* @param {string} content
*/
module.exports = function (content) {
// 标记为可缓存
this.cacheable();
// 获取并验证选项
const options = this.getOptions(schema);
// 异步回调
const callback = this.async();
const { prefix = "icon-" } = options;
// 1. 获取当前 SVG 文件路径
const svgPath = this.resourcePath;
// 2. 提取纯净的文件名(不含扩展名和路径)
const fileName = path.basename(svgPath, ".svg");
// 3. 生成 CSS 类名
const className = `${prefix}${fileName}`;
console.log(`transform svg: ${fileName}`);
try {
// 4. 优化 SVG 内容(可选)
const optimizedSvg = content
.replace(/\s+/g, " ") // 简化空格
.replace(/#/g, "%23") // 转义 # 符号
.replace(/"/g, "'"); // 统一引号
// 5. 转换为 Base64 Data URL
const base64 = Buffer.from(optimizedSvg).toString("base64");
const dataUrl = `data:image/svg+xml;base64,${base64}`;
// 6. 生成 CSS 规则
// SVG 默认作为mask
const cssMaskRule = `
.${className} {
mask-image: url("${dataUrl}");
mask-repeat: no-repeat;
mask-size: contain;
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
}
`;
// SVG 作为背景图
const cssBgRule = `
.${className}?bg {
background-image: url("${dataUrl}");
background-repeat: no-repeat;
background-size: contain;
display: inline-block;
width: 1em;
height: 1em;
}
`;
callback(null, `${cssMaskRule} ${cssBgRule}`);
} catch (error) {
callback(error);
}
};
新建 src/icons 文件夹,里面放入几个 SVG 文件。

Webpack 配置
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const svgIconLoader = path.resolve(__dirname, "svg-icon-loader");
const iconPath = path.resolve(__dirname, "src/icons"); // SVG目录
module.exports = {
// ...其他配置
module: {
rules: [
// ... 其他配置
{
test: /\.svg$/,
include: [iconPath],
use: [
"style-loader", // 将 CSS 注入 DOM
"css-loader", // 解析 CSS
{
loader: svgIconLoader,
options: {
prefix: "icon-",
},
},
],
},
],
},
};
使用
src/App.jsx
const App = () => {
return (
<div style={{ color: "black", fontSize: 36 }}>
<div className="icon-sun"></div>
<div className="icon-moon"></div>
<div className="icon-user"></div>
</div>
);
};
export default App
执行 pnpm run start 发现没效果,因为 Webpack 默认只会处理在代码中显式引用的文件,如果要自动导入的话,就需要通过 plugin 实现。
后面学完 plugin 刚刚好好就自定义这个 plugin 吧!!!
这里先手动引入测试下,在 App.jsx 中添加:
import "./icons/moon.svg"
import "./icons/sun.svg"
import "./icons/user.svg"
运行效果:

总结
loader 的本质就是一个转换函数,掌握好其特性,遵循其开发规范的话,自定义 loader 还是挺简单的。