写 loader 时,函数里总是用到 this.getOptions()、this.async()、this.addDependency() 这些东西。它们并不是全局 API,而是 webpack 在执行 loader 时注入的上下文对象。这个上下文提供了配置读取、异步控制、路径信息、依赖管理等能力。本文系统梳理 loader 可用的 API。
一、loader 里的 this 是什么
当 webpack 调用一个 loader 时,会以「caller」的形式把 loader 函数执行在一个**上下文对象(loader context)**上:
module.exports = function (source) {
// 这个 this 就是 loader context
console.log(this.resourcePath);
return source;
};
所以:
- 不要用箭头函数写 loader,否则拿不到
this。 - 这个
this由 webpack 注入,提供了下文所有 API。
二、读取配置:getOptions
this.getOptions(schema)
获取 loader 的配置项,是读取 options 的标准方式:
module.exports = function (source) {
const options = this.getOptions(); // 等价于 loader 的 options
return source.replace(/__NAME__/g, options.name);
};
它还支持 JSON Schema 校验,配置不合法时直接报错:
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
};
module.exports = function (source) {
const options = this.getOptions(schema);
// ...
};
旧代码里的
this.query是它的前身,已不推荐使用;当 options 是字符串时它才有值。新代码一律用this.getOptions()。
三、同步与异步控制
this.async()
声明这是一个异步 loader,返回一个 callback:
module.exports = function (source) {
const callback = this.async();
setTimeout(() => {
callback(null, source + '\n// async');
}, 100);
};
this.callback(err, content, sourceMap, meta)
用于同步或异步返回结果,并携带 Source Map 与元信息:
module.exports = function (source) {
// 同步也可以使用 this.callback
this.callback(null, source, null, { custom: true });
};
需要注意:
- 调用
this.async()后必须使用它返回的 callback,且只能调用一次。 - 使用
this.callback时不要再return内容。 meta会传递给下一个 loader,可通过this.data之外的方式传递额外信息。
四、路径与请求信息
这些属性帮你判断「当前在处理哪个文件」:
| API | 含义 | 示例 |
|---|---|---|
this.resourcePath | 资源文件的绝对路径 | /src/style.css |
this.resourceQuery | 资源路径的查询串 | ?modules |
this.resource | resourcePath + resourceQuery | /src/style.css?modules |
this.context | 资源所在目录 | /src |
this.rootContext | 项目根目录 | /project |
this.request | 完整的请求串(loaders + 资源) | /a-loader!./b.css |
this.remainingRequest | 当前 loader 右侧剩余部分 | css-loader!./b.css |
this.previousRequest | 当前 loader 左侧部分 | a-loader |
this.currentRequest | 当前 loader 自身的请求 | b-loader |
this.loaders | 整条 loader 链的数组 | [{...}, {...}] |
this.loaderIndex | 当前 loader 在链中的下标 | 1 |
这些信息常用于:
- 根据文件路径决定是否处理(如只处理
src下的文件)。 - 根据
resourceQuery区分不同处理方式。 - 在
pitch中利用remainingRequest/previousRequest重组请求。
五、依赖管理(watch 相关)
当 loader 的处理依赖于其他文件时,必须把这些文件声明为依赖,否则文件变化时 webpack 不会重新构建:
| API | 作用 |
|---|---|
this.addDependency(file) | 添加单个文件依赖 |
this.addContextDependency(dir) | 添加目录依赖(目录内文件变化都触发重建) |
this.addMissingDependency(file) | 声明一个「当前不存在但未来可能创建」的文件依赖 |
this.clearDependencies() | 清空已收集的依赖 |
const fs = require('fs');
module.exports = function (source) {
const configPath = path.resolve(__dirname, 'config.json');
this.addDependency(configPath); // 配置变化时重新执行本 loader
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
return source.replace(/__CONFIG__/g, JSON.stringify(config));
};
六、输出文件与错误提示
this.emitFile(name, content, sourceMap, assetInfo)
让 loader 直接向产物目录写出一个文件:
module.exports = function (source) {
this.emitFile('generated/report.txt', 'some content');
return source;
};
this.emitWarning(warning) / this.emitError(error)
输出警告或错误。emitWarning 不中断构建,emitError 会记录错误:
this.emitWarning(new Error('这里有个可疑的写法'));
this.emitError(new Error('必须修复的错误'));
this.getLogger(name)
获取一个 logger,输出结构化的日志:
const logger = this.getLogger('my-loader');
logger.warn('构建警告');
logger.error('构建错误');
七、解析与加载其他模块
有时 loader 需要在内部解析路径或加载别的模块。
this.resolve(context, request, callback)
解析一个请求的绝对路径:
module.exports = function (source) {
const callback = this.async();
this.resolve(this.context, './config.js', (err, resolvedPath) => {
if (err) return callback(err);
callback(null, `/* resolved: ${resolvedPath} */\n${source}`);
});
};
不传 callback 时返回 Promise。this.getResolve(options) 可以拿到一个带自定义解析选项的 resolve 函数。
this.loadModule(request, callback)
加载并编译另一个模块,拿到其源码:
this.loadModule('./other.js', (err, source, sourceMap, module) => {
// ...
});
this.importModule(request, options, callback)
以模块方式导入,支持 layer 等高级选项;不传 callback 时返回 Promise。
八、环境与编译信息
| API | 含义 |
|---|---|
this.mode | 当前模式:'development' / 'production' / 'none' |
this.target | 编译目标(如 'web'、'node') |
this.sourceMap | 是否需要生成 Source Map |
this.hot | 是否开启了热更新 |
this.environment | 目标环境支持的语法特性信息 |
this.webpack | 是否为 webpack 环境(布尔值) |
this.version | webpack 版本号 |
this.fs | 输入文件系统,用于读取文件 |
this.data | 在 pitch 与 normal 之间共享的数据对象 |
this.data 在上一篇文章讲 pitch 时提过,是同一个 loader 的 pitch 与 normal 阶段之间传递数据的容器:
module.exports = function (source) {
return `/* start: ${this.data.start} */\n${source}`;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.start = Date.now();
};
this.cacheable()在早期版本用于声明结果可缓存,webpack 5 中已基本废弃,通常无需调用。
九、一个综合示例
下面这个 loader 组合使用了多个 API——读取配置、声明依赖、异步解析、输出警告:
const fs = require('fs');
const path = require('path');
module.exports = function (source) {
const callback = this.async();
const options = this.getOptions();
const configPath = path.resolve(this.rootContext, options.config || 'app.config.json');
// 声明依赖:配置文件变化时重新构建
this.addDependency(configPath);
this.resolve(this.context, options.template || './banner.txt', (err, bannerPath) => {
let banner = '';
if (!err) {
this.addDependency(bannerPath);
banner = fs.readFileSync(bannerPath, 'utf-8');
} else {
this.emitWarning(new Error('未找到 banner 模板,已跳过'));
}
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const result = `${banner}\n/* env: ${config.env} */\n${source}`;
callback(null, result);
});
};
十、注意事项
- 不要用箭头函数写 loader,否则
this不是 loader context。 - 读取选项用
this.getOptions(),不要再用this.query。 - 异步 loader 必须调用 callback 且仅一次,否则构建挂起。
- 依赖文件一定要
addDependency,否则 watch 模式下改动不触发重建。 - 只用公开 API,
this._compiler、this._module等下划线属性属于内部实现,随时可能变。 - 保持 loader 无副作用,写产物文件用
emitFile这类受控方式,而不是直接操作文件系统写输出。
小结
| 类别 | 代表 API |
|---|---|
| 读取配置 | getOptions |
| 同步/异步 | async、callback |
| 路径信息 | resourcePath、resourceQuery、context、request |
| 依赖管理 | addDependency、addContextDependency、addMissingDependency |
| 输出与提示 | emitFile、emitWarning、emitError、getLogger |
| 解析加载 | resolve、getResolve、loadModule、importModule |
| 环境信息 | mode、target、sourceMap、hot、webpack、version |
| 阶段传值 | data |
loader context 是 webpack 给 loader 的「能力接口」。记住它大致分为「读配置、控异步、拿路径、报依赖、做输出、解模块」几类,写 loader 时就不会面对一个空白的函数不知所措了。