Loader
Loader 在 Webpack 构建链中把源文件转为可打包的 JS 模块(如 TS、SCSS、Vue SFC)。见 工作原理。
一、Loader 执行顺序
Loader 的执行顺序遵循 从右向左、从下到上 的原则,但通过 enforce 可以改变优先级。
1. 默认顺序(normal loader)
在配置数组中,Loader 从后往前执行(类似洋葱模型)。例如:
module: {
rules: [{ test: /\.js$/, use: ["loader-c", "loader-b", "loader-a"] }];
}
执行顺序:loader-a → loader-b → loader-c。
2. enforce 分类
可以为 Loader 指定 enforce 属性,分为四类,执行顺序为:
pre → normal → inline → post
- pre:前置 loader(
enforce: 'pre') - normal:普通 loader(默认)
- inline:内联 loader(在 import 语句中直接使用,如
import '!style-loader!css-loader!./a.css') - post:后置 loader(
enforce: 'post')
示例:
rules: [
{ test: /\.js$/, loader: "pre-loader", enforce: "pre" },
{ test: /\.js$/, loader: "normal-loader" }, // enforce 默认 'normal'
{ test: /\.js$/, loader: "post-loader", enforce: "post" },
];
执行顺序:pre-loader → normal-loader → post-loader。如果有内联 loader,会在 normal 之后、post 之前执行。
3. 内联 loader 的覆盖符号
在 import 语句中,可以使用前缀改变 loader 执行顺序:
!禁用 normal loader-!禁用 pre 和 normal loader!!禁用所有除了内联 loader
// 只使用 style-loader 和 css-loader,跳过 pre/normal/post
import "!!style-loader!css-loader!./a.css";
二、Loader 的输入与输出
Loader 接收文件内容(字符串或 Buffer),返回转换后的内容。
1. 同步 Loader
直接 return 转换后的内容。
module.exports = function (source) {
// source 是原始文件内容
const result = source.replace(/console\.log\(.*?\)/g, "");
return result;
};
也可以使用 this.callback 返回多个值:
module.exports = function (source) {
const result = process(source);
this.callback(null, result, sourceMap, meta);
// 不返回值 (return;)
};
this.callback 参数:
error:Error 或 nullcontent:转换后的代码(String 或 Buffer)sourceMap:可选的 source mapmeta:可选的元数据(如 AST 等)
2. 异步 Loader
使用 this.async() 获取一个回调函数。
module.exports = function (source) {
const callback = this.async();
someAsyncProcess(source, (err, result) => {
if (err) return callback(err);
callback(null, result);
});
};
3. 返回多种内容
- 内容类型:可以是 JavaScript 代码字符串,也可以是其他语言(如 CSS、HTML)—— Webpack 会继续用其他 Loader 处理。
- Source Map:便于调试转换后的代码。
- 元数据:例如在 loader 中生成 AST,可以传递给下一个 loader。
三、Loader 工具库
1. loader-utils
提供常用的辅助函数。
const { getOptions, interpolateName } = require("loader-utils");
module.exports = function (source) {
// 获取 loader 配置中的 options
const options = getOptions(this);
// 生成文件名(带 hash 等)
const filename = interpolateName(this, "[name].[hash].[ext]", {
content: source,
context: this.rootContext,
});
// 发出一个文件(例如生成图片)
this.emitFile(filename, source);
return `module.exports = ${JSON.stringify(filename)};`;
};
interpolateName:支持占位符如[name]、[ext]、[hash]、[contenthash]、[path]等。getOptions:从 loader context 中安全地提取 options。parseQuery:解析类似 URL 的 query 字符串(较少用)。stringifyRequest:将模块请求转换为相对于当前文件的相对路径。
2. schema-utils
校验 loader options 的 JSON Schema。
const { validate } = require("schema-utils");
const schema = {
type: "object",
properties: {
test: { type: "string" },
limit: { type: "number" },
},
};
module.exports = function (source) {
const options = getOptions(this);
validate(schema, options, { name: "My Loader" });
// ...
};
四、自定义 Loader 典型场景
1. 语言预处理(替换模板变量)
例如,将文件中的 {{VERSION}} 替换为构建时的版本号。
// version-loader.js
module.exports = function (source) {
const version = process.env.npm_package_version || "0.0.0";
return source.replace(/{{VERSION}}/g, version);
};
2. 按需引入替换(优化组件库导入)
将 import { Button, Input } from 'ui-library' 转换为具体路径,实现按需引入。
// optimize-import-loader.js
module.exports = function (source) {
return source.replace(
/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]ui-library['"]/g,
(match, names) => {
const imports = names
.split(",")
.map((name) => {
const trimmed = name.trim();
return `import ${trimmed} from 'ui-library/lib/${trimmed}';`;
})
.join("\n");
return imports;
},
);
};
3. 移除调试代码(console.log、debugger)
// strip-debug-loader.js
module.exports = function (source) {
const result = source.replace(/console\.(log|debug|info|warn)\(.*?\)\s*;?/g, "");
return result.replace(/debugger\s*;?/g, "");
};
配合 enforce: 'pre' 确保在其他 loader 之前执行。
五、Pitch Loader(熔断机制)
每个 Loader 除了默认的 (默认的转换函数) 外,还可以导出一个 pitch 方法。pitch 在 loader 执行顺序的第一阶段被调用,它有机会短路后续的 loader。
1. pitch 的执行顺序
对于一组 loader [a, b, c]:
- 先调用
a.pitch→b.pitch→c.pitch - 如果某个
pitch返回了非undefined的值,则停止执行后面的 pitch,并开始返回阶段:直接返回该结果给上一个 loader 的 normal 函数。
2. 短路机制
如果 b.pitch 返回了一个值,那么:
- 不再调用
c.pitch和c的 normal 函数。 b的 normal 函数不会被调用(因为 pitch 已经返回)。- 该值直接返回给
a的 normal 函数(作为输入)。 - 然后执行
a的 normal 函数,最后输出。
示意图:
normal 执行顺序: a → b → c
pitch 执行顺序: a.pitch → b.pitch → c.pitch
如果 b.pitch 返回内容,则跳过 c 的所有函数,并且 b 的 normal 也不执行,结果回传给 a.normal。
3. pitch 的典型用途
- 提前返回:如果 loader 已经知道最终输出(例如文件是空的、不需要处理),可以直接返回。
- 收集信息:在 pitch 阶段读取配置或修改请求路径(例如
style-loader使用 pitch 将 CSS 注入到 JS 中)。 - 动态添加依赖:
this.addDependency等。
示例:一个简单的 pitch loader,跳过后续处理
// skip-loader.js
module.exports = function (source) {
// 正常情况下不会被调用(如果 pitch 返回了值)
return source;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
// 直接返回一段代码,不再执行后面的 loader
return `module.exports = "skipped";`;
};
4. pitch 参数说明
remainingRequest:剩余请求字符串(从当前 loader 之后的所有 loader 加上资源路径)。precedingRequest:前面的请求字符串。data:可在 pitch 和 normal 之间共享的对象。
总结
| 方面 | 关键点 |
|---|---|
| 执行顺序 | enforce: 'pre' > normal > inline > 'post';默认从右向左执行 |
| 同步/异步 | 同步用 return 或 this.callback;异步用 this.async() |
| 工具库 | loader-utils(参数、命名)、schema-utils(校验) |
| 典型场景 | 变量替换、按需引入移除调试代码 |
| Pitch Loader | 短路后续 loader,用于提前返回或修改请求 |
通过开发自定义 Loader,可以定制 Webpack 处理各种文件的方式,满足工程化中的特殊需求。高级用法常与 pitch 配合,实现类似 style-loader 的动态注入逻辑。
参考文献
以下链接在编写时均可正常访问:
相关文章
工作原理
Webpack 是静态模块打包器:从入口递归解析依赖,经 Loader 转换后输出一个或多个 bundle。与 Vite 对比见 Webpack 与 Vite。
产物优化
Webpack 通过代码分割、Tree Shaking、压缩与资源模块等减少体积、改善缓存。原理背景见 工作原理。
企业级实践
大型前端项目在 Webpack 场景下的多环境拆分、Docker 构建缓存、自定义 CLI 与微前端(Module Federation / single-spa)等实践。入门见 工程化概览。
进阶配置
Webpack 进阶场景:多页面(MPA)、devServer、环境变量注入、Source Map、Module Federation 等。基础见 工作原理。
Plugin
Webpack 插件在构建生命周期钩子上扩展能力(修改资源、生成 HTML、上传 CDN 等)。背景见 工作原理;与 Loader 分工:Loader 转换单文件,插件面向整体构建。
其他配置
除 ESLint、Prettier 等规范文件外,仓库中常见的工具链与包管理配置如下。见 工程化概览。