Webpack:Loader Interface
loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。函数中的 this 作为上下文会被 webpack 填充,并且 loader runner 中包含一些实用的方法,比如可以使 loader 调用方式变为异步,或者获取 query 参数。
起始 loader 只有一个入参:资源文件的内容。compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为 String 或者 Buffer(能够被转换为 string)类型,代表了模块的 JavaScript 源码。另外,还可以传递一个可选的 SourceMap 结果(格式为 JSON 对象)。
如果是单个处理结果,可以在同步模式中直接返回。如果有多个处理结果,则必须调用 this.callback()。在异步模式中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。
/**
*
* @param {string|Buffer} content 源文件的内容
* @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 数据
* @param {any} [meta] meta 数据,可以是任何内容
*/
function webpackLoader(content, map, meta) {
// 你的 webpack loader 代码
}
示例
以下部分提供了不同类型的 loader 的一些基本示例。注意,map 和 meta 参数是可选的,查看下面的this.callback。
同步 Loaders
无论是 return 还是 this.callback 都可以同步地返回转换后的 content 值:
sync-loader.js
module.exports = function (content, map, meta) {
return someSyncOperation(content);
};
this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content。
sync-loader-with-multiple-results.js
module.exports = function (content, map, meta) {
this.callback(null, someSyncOperation(content), map, meta);
return; // 当调用 callback() 函数时,总是返回 undefined
};
异步 Loaders
对于异步 loader,使用 this.async 来获取 callback 函数:
async-loader.js
module.exports = function (content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function (err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
async-loader-with-multiple-results.js
module.exports = function (content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function (err, result, sourceMaps, meta) {
if (err) return callback(err);
callback(null, result, sourceMaps, meta);
});
};
"Raw" Loader
默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。
raw-loader.js
module.exports = function (content) {
assert(content instanceof Buffer);
return someSyncOperation(content);
// 返回值也可以是一个 `Buffer`
// 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
Pitching Loader
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。
对于以下 use 配置:
module.exports = {
//...
module: {
rules: [
{
//...
use: ['a-loader', 'b-loader', 'c-loader'],
},
],
},
};
将会发生这些步骤:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
那么,为什么 loader 可以利用 "pitching" 阶段呢?
首先,传递给 pitch 方法的 data,在执行阶段也会暴露在 this.data 之下,并且可以用于在循环时,捕获并共享前面的信息。
module.exports = function (content) {
return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
其次,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loader 的 pitch 方法返回了一些东西:
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return (
'module.exports = require(' +
JSON.stringify('-!' + remainingRequest) +
');'
);
}
};
上面的步骤将被缩短为:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
The Loader Context
loader context 表示在 loader 内使用 this 可以访问的一些方法或属性。
loader 上下文示例
下面提供一个例子,将使用 require 进行调用:
在 /abc/file.js 中:
require('./loader1?xyz!loader2!./resource?rrr');
this.addContextDependency
addContextDependency(directory: string)
添加目录作为 loader 结果的依赖。
this.addDependency
addDependency(file: string)
dependency(file: string) // shortcut
添加一个文件作为产生 loader 结果的依赖,使它们的任何变化可以被监听到。例如,sass-loader, less-loader 就使用了这个技巧,当它发现无论何时导入的 css 文件发生变化时就会重新编译。
this.addMissingDependency
addMissingDependency(file: string)
添加一个不存在的文件作为 loader 结果的依赖项,以使它们可监听。类似于 addDependency,但是会在正确附加观察者之前处理在编译期间文件的创建。
this.async
告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。
this.cacheable
设置是否可缓存标志的函数:
cacheable(flag = true: boolean)
默认情况下,loader 的处理结果会被标记为可缓存。调用这个方法然后传入 false,可以关闭 loader 处理结果的缓存能力。
一个可缓存的 loader 在输入和相关依赖没有变化时,必须返回相同的结果。这意味着 loader 除了 this.addDependency 里指定的以外,不应该有其它任何外部依赖。
this.callback
可以同步或者异步调用的并返回多个结果的函数。预期的参数是:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
- 第一个参数必须是 Error 或者 null
- 第二个参数是一个 string 或者 Buffer。
- 可选的:第三个参数必须是一个可以被 this module 解析的 source map。
- 可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
this.clearDependencies
clearDependencies();
移除 loader 结果的所有依赖,甚至自己和其它 loader 的初始依赖。考虑使用 pitch。
this.context
模块所在的目录 可以用作解析其他模块成员的上下文。
在我们的 例子 中:因为 resource.js 在这个目录中,这个属性的值为 /abc
this.data
在 pitch 阶段和 normal 阶段之间共享的 data 对象。
this.emitError
emitError(error: Error)
emit 一个错误,也可以在输出中显示。
ERROR in ./src/lib.js (./src/loader.js!./src/lib.js)
Module Error (from ./src/loader.js):
Here is an Error!
@ ./src/index.js 1:0-25
this.emitFile
emitFile(name: string, content: Buffer|string, sourceMap: {...})
产生一个文件。这是 webpack 特有的。
this.emitWarning
emitWarning(warning: Error)
发出一个警告,在输出中显示如下:
WARNING in ./src/lib.js (./src/loader.js!./src/lib.js)
Module Warning (from ./src/loader.js):
Here is a Warning!
@ ./src/index.js 1:0-25
this.fs
用于访问 compilation 的 inputFileSystem 属性。
this.getOptions(schema)
提取给定的 loader 选项,接受一个可选的 JSON schema 作为参数
this.getResolve
getResolve(options: ResolveOptions): resolve
resolve(context: string, request: string, callback: function(err, result: string))
resolve(context: string, request: string): Promise<string>
创建一个类似于 this.resolve 的解析函数。
在 webpack resolve 选项 下的任意配置项都是可能的。他们会被合并进 resolve 配置项中。请注意,"..." 可以在数组中使用,用于拓展 resolve 配置项的值。例如:{ extensions: [".sass", "..."] }。
options.dependencyType 是一个额外的配置。它允许我们指定依赖类型,用于从 resolve 配置项中解析 byDependency。
解析操作的所有依赖项都会自动作为依赖项添加到当前模块中。
this.hot
loaders 的 HMR(热模块替换)相关信息。
module.exports = function (source) {
console.log(this.hot); // true if HMR is enabled via --hot flag or webpack configuration
return source;
};
this.importModule
5.32.0+
this.importModule(request, options, [callback]): Promise
一种可以选择的轻量级解决方案,用于子编译器在构建时编译和执行请求。
- request: 加载模块的请求字符串
- options:layer:指定该模块放置/编译的层publicPath:用于构建模块的公共路径
- callback:一个可选的 Node.js 风格的回调,返回模块的 exports 或 ESM 的命名空间对象。如果没有提供回调,importModule将返回一个 Promise。
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /stylesheet\.js$/i,
use: ['./a-pitching-loader.js'],
type: 'asset/source', // 我们将 type 设置为 'asset/source',其会返回一个字符串。
},
],
},
};
a-pitching-loader.js
exports.pitch = async function (remaining) {
const result = await this.importModule(
this.resourcePath + '.webpack[javascript/auto]' + '!=!' + remaining
);
return result.default || result;
};
src/stylesheet.js
import { green, red } from './colors.js';
export default `body { background: ${red}; color: ${green}; }`;
src/colors.js
export const red = '#f00';
export const green = '#0f0';
src/index.js
import stylesheet from './stylesheet.js';
// stylesheet 在构建时会成为一个字符串:`body { background: #f00; color: #0f0; }`。
在上面的例子中你可能会注意到一些东西:
- 我们有一个 pitching loader,
- 我们在 pitching loader 中使用 !=! 语法来为请求设置 matchResource,例如,我们将使用
this.resourcePath
+'.webpack[javascript/auto]'
而不是原始资源匹配 module.rules, -
.webpack[javascript/auto]
是.webpack[type]
模式的伪拓展,当没有指定其他模块类型时,我们使用它指定一个默认 模块类型,它通常和 !=! 语法一起使用。
注意,上面的示例是一个简化的示例,你可以查看 webpack 仓库的完整示例。
this.loaderIndex
当前 loader 在 loader 数组中的索引。
在示例中:loader1 中得到:0,loader2 中得到:1
this.loadModule
loadModule(request: string, callback: function(err, source, sourceMap, module))
解析给定的 request 到模块,应用所有配置的 loader,并且在回调函数中传入生成的 source、sourceMap 和模块实例(通常是 NormalModule 的一个实例)。如果你需要获取其他模块的源代码来生成结果的话,你可以使用这个函数。
this.loadModule 在 loader 上下文中默认使用 CommonJS 来解析规则。用一个合适的 dependencyType 使用 this.getResolve。例如,在使用不同的语义之前使用 'esm'、'commonjs' 或者一个自定义的。
this.loaders
所有 loader 组成的数组。它在 pitch 阶段的时候是可以写入的。
loaders = [{request: string, path: string, query: string, module: function}]
在此示例中:
[
{
request: '/abc/loader1.js?xyz',
path: '/abc/loader1.js',
query: '?xyz',
module: [Function],
},
{
request: '/abc/node_modules/loader2/index.js',
path: '/abc/node_modules/loader2/index.js',
query: '',
module: [Function],
},
];
this.mode
当 webpack 运行时读取 mode 的值
可能的值为:'production', 'development', 'none'
this.query
- 如果这个 loader 配置了 options 对象的话,this 就指向这个对象。
- 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。
this.request
被解析出来的 request 字符串。
在我们的示例中:'/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr'
this.resolve
resolve(context: string, request: string, callback: function(err, result: string))
像 require 表达式一样解析一个 request。
- context 必须是一个目录的绝对路径。此目录用作解析的起始位置。
- request 是要被解析的 request。通常情况下,像 ./relative 的相对请求或者像 module/path 的模块请求会被使用,但是像 /some/path 也有可能被当做 request。
- callback 是一个给出解析路径的 Node.js 风格的回调函数。
解析操作的所有依赖项都会自动作为依赖项添加到当前模块中。
this.resource
request 中的资源部分,包括 query 参数。
在示例中:'/abc/resource.js?rrr'
this.resourcePath
资源文件的路径。
在【示例](#example-for-the-loader-context)中:'/abc/resource.js'
this.resourceQuery
资源的 query 参数。
在示例中:'?rrr'
this.rootContext
从 webpack 4 开始,原先的 this.options.context 被改为 this.rootContext。
this.sourceMap
是否应该生成一个 source map。因为生成 source map 可能会非常耗时,你应该确认 source map 确实需要。
this.target
compilation 的目标。从配置选项中传递。
示例:'web', 'node'
this.utils
5.27.0+
可以访问 contextify 与 absolutify 功能。
- contextify: 返回一个新的请求字符串,尽可能避免使用绝对路径。
- absolutify: 尽可能使用相对路径返回一个新的请求字符串。
my-sync-loader.js
module.exports = function (content) {
this.utils.contextify(
this.context,
this.utils.absolutify(this.context, './index.js')
);
this.utils.absolutify(this.context, this.resourcePath);
// …
return content;
};
this.version
loader API 的版本号 目前是 2。这对于向后兼容性有一些用处。通过这个版本号,你可以自定义逻辑或者降级处理。
this.webpack
如果是由 webpack 编译的,这个布尔值会被设置为 true。
Webpack 特有属性
loader 接口提供所有模块的相关信息。然而,在极少数情况下,你可能需要访问 compiler api 本身。
因此,你应该把它们作为最后的手段。使用它们将降低 loader 的可移植性。
this._compilation
用于访问 webpack 的当前 Compilation 对象。
this._compiler
用于访问 webpack 的当前 Compiler 对象。
过时的上下文属性
由于我们计划将这些属性从上下文中移除,因此不鼓励使用这些属性。它们仍然列在这里,以备参考。
this.debug
一个布尔值,当处于 debug 模式时为 true。
this.inputValue
从上一个 loader 那里传递过来的值。如果你会以模块的方式处理输入参数,建议预先读入这个变量(为了性能因素)。
this.minimize
决定处理结果是否应该被压缩。
this.value
向下一个 loader 传值。如果你知道了作为模块执行后的结果,请在这里赋值(以元素数组的形式)。
this._module
一种 hack 写法。用于访问当前加载的 Module 对象。
错误报告
您可以通过以下方式从 loader 内部报告错误:
- 使用 this.emitError. 将在不中断模块编译的情况下报告错误。
- 使用 throw(或其他未捕获的意外异常)。loader 运行时引发错误将导致当前模块编译失败。
- 使用 callback(异步模式)。向回调传递错误也会导致模块编译失败。
示例:
./src/index.js
require('./loader!./lib');
从 loader 当中抛出错误:
./src/loader.js
module.exports = function (source) {
throw new Error('This is a Fatal Error!');
};
或者在异步模式下,传入一个错误给 callback:
./src/loader.js
module.exports = function (source) {
const callback = this.async();
//...
callback(new Error('This is a Fatal Error!'), source);
};
这个模块将获取像下面的 bundle:
/***/ "./src/loader.js!./src/lib.js":
/*!************************************!*\
!*** ./src/loader.js!./src/lib.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports) {
throw new Error("Module build failed (from ./src/loader.js):\nError: This is a Fatal Error!\n at Object.module.exports (/workspace/src/loader.js:3:9)");
/***/ })
然后构建输出结果将显示错误,与 this.emitError 相似:
ERROR in ./src/lib.js (./src/loader.js!./src/lib.js)
Module build failed (from ./src/loader.js):
Error: This is a Fatal Error!
at Object.module.exports (/workspace/src/loader.js:2:9)
@ ./src/index.js 1:0-25
如下所示,不仅有错误消息,还提供了有关所涉及的 loader 和模块的详细信息:
- 模块路径:ERROR in ./src/lib.js
- request 字符串:(./src/loader.js!./src/lib.js)
- loader 路径:(from ./src/loader.js)
- 调用路径:@ ./src/index.js 1:0-25
Inline matchResource
在 webpack v4 中引入了一种新的内联请求语法。前缀为 <match-resource>!=! 将为此请求设置 matchResource。
当 matchResource 被设置时,它将会被用作匹配 module.rules 而不是源文件。如果需要对资源应用进一步的 loader,或者需要更改模块类型,这可能会很有用。 它也显示在统计数据中,用于匹配 Rule.issuer 和 test in splitChunks。
示例:
file.js
/* STYLE: body { background: red; } */
console.log('yep');
loader 可以将文件转换为以下文件,并使用 matchResource 应用用户指定的 CSS 处理规则:
file.js (transformed by loader)
import './file.js.css!=!extract-style-loader/getStyles!./file.js';
console.log('yep');
这将会向 extract-style-loader/getStyles!./file.js 中添加一个依赖,并将结果视为 file.js.css。因为 module.rules 有一条匹配 /\.css$/ 的规则,并且将会应用到依赖中。
这个 loader 就像是这样:
extract-style-loader/index.js
const getStylesLoader = require.resolve('./getStyles');
module.exports = function (source) {
if (STYLES_REGEXP.test(source)) {
source = source.replace(STYLES_REGEXP, '');
return `import ${JSON.stringify(
this.utils.contextify(
this.context || this.rootContext,
`${this.resource}.css!=!${getStylesLoader}!${this.remainingRequest}`
)
)};${source}`;
}
return source;
};
extract-style-loader/getStyles.js
module.exports = function (source) {
const match = source.match(STYLES_REGEXP);
return match[0];
};
Logging
自 webpack 4.37 发布以来,Logging API 就可用了。当 stats configuration 或者 infrastructure logging 中启用 logging 时,loader 可以记录消息,这些消息将以相应的日志格式(stats,infrastructure)打印出来。
- Loaders 最好使用 this.getLogger() 进行日志记录,这是指向 compilation.getLogger() 具有 loader 路径和已处理的文件。这种日志记录被存储到 Stats 中并相应地格式化。它可以被 webpack 用户过滤和导出。
- Loaders 可以使用 this.getLogger('name') 获取具有子名称的独立记录器。仍会添加 loader 路径和已处理的文件。
- Loaders 可以使用特殊的回退逻辑来检测日志支持 this.getLogger() ? this.getLogger() : console。在使用不支持 getLogger 方法的旧 webpack 版本时提供回退方法。