正如您所看到的那样,loader 是 Webpack 构建中的一部分。如果要加载资源,则很可能需要配置一个匹配的 loader。尽管有很多可用的 loader,但可能没有一个能够满足你的需要。
接下来你将学会开发几个小型 loader。但在此之前,了解如何单独调试它们是很好的。
如果您想要一个独立的 loader 或插件项目作为一个好的开始,请考虑使用 webpack-defaults。它提供了一个基本的起点,包括 linting、测试和其他好的工具。
使用 loader-runner 调试 loader
loader-runner 允许您在 Webpack 之外运行 loader,让您可以了解有关 loader 开发的更多信息。首先安装它:
npm install loader-runner --save-dev
首先写一个例子进行测试,这个例子直接将输入叠加并返回:
loaders/demo-loader.js
module.exports = input => input + input;
设置要处理的文件:
demo.txt
foobar
代码中没有特定的与 Webpack 相关的东西。下一步是通过 loader-runner 运行 loader:
run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
如果现在运行脚本(node run-loader.js
),你应该看到以下输出:
{
result: [ 'foobar\nfoobar\n' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: []
}
通过输出信息,我们可以看到处理结果
,该资源被当做 buffer 处理,另外还可以看到其他元信息。这些数据足以开发更复杂的 loader 了。
如果要将输出信息保存到文件,请使用 Node 文档中讨论的
fs.writeFileSync("./output.txt", result.result)
或其异步版本。
对于安装到本地项目中的 loader,我们可以直接通过其名称进行引用而不是解析它们的完整路径。例如:
loaders: ["raw-loader"]
。
实现异步 loader
尽管您可以使用同步接口实现许多 loader,但有时需要进行异步计算。另外,在将第三方软件包包装为 loader 时,还必须采用异步方式。
上面的示例可以通过使用 Webpack 特定的 API this.async()
转换成异步形式。Webpack 会设置 this,并且该函数遵循 Node 约定(错误优先,然是是结果)返回一个回调函数。
调整如下:
loaders/demo-loader.js
module.exports = function(input) {
const callback = this.async();
// callback 不存在 -> 返回同步结果
// if (callback) { ... }
callback(null, input + input);
};
鉴于 Webpack 通过
this
注入其API,所以此处不能使用箭头函数(() => ...
)。
如果要将源映射传递给 Webpack,请将其作为回调的第三个参数。
再次运行演示脚本(node run-loader.js
)应该得到与以前相同的结果。要在执行期间抛出错误,请尝试以下操作:
loaders/demo-loader.js
module.exports = function(input) {
const callback = this.async();
callback(new Error("Demo error"));
};
结果应该包含 Error: Demo error
,以及与之相关的堆栈跟踪信息,显示错误发生的位置。
仅返回输出
loader 也可用于只返回输出。代码实现如下:
loaders/demo-loader.js
module.exports = function() {
return "foobar";
};
但重点是什么呢?您可以通过 Webpack 入口传递一些东西给 loader。大多数情况下,入口都是指向一些已经存在的文件,而这种方式使您可以动态生成代码再传递给 loader。
如果要返回
Buffer
,请设置module.exports.raw = true
。该标志将覆盖期望返回字符串的默认行为。
写文件
有一些 loader,如 file-loader,用来发出文件。Webpack 为此提供了一个单独的方法 this.emitFile
。鉴于 loader-runner 中没有实现它,你必须模拟一个:
run-loader.js
...
runLoaders(
{
resource: "./demo.txt",
loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
要完全实现 file-loader,您必须做两件事:发出文件并返回它的路径。你可以按如下方式来实现:
loaders/demo-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
content,
});
this.emitFile(url, content);
const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
return `export default ${path}`;
};
Webpack 提供了两种额外的 emit
方法:
this.emitWarning(<string>)
this.emitError(<string>)
我们也可以使用 console
中的相关方法来替代它们。另外,对于 this.emitFile
,你必须模拟它才能让 loader-runner 工作。
接下来的问题是,如何将文件名传递给加载器。
将选项传递给 loader
为了更方便地演示如何获取传入的选项值,run-loader 需要进行一些小的调整:
run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./demo.txt",
// loaders: [path.resolve(__dirname, "./loaders/demo-loader")],
loaders: [
{
loader: path.resolve(__dirname, "./loaders/demo-loader"),
options: {
name: "demo.[ext]",
},
},
],
context: {
emitFile: () => {},
},
readResource: fs.readFile.bind(fs),
},
(err, result) => (err ? console.error(err) : console.log(result))
);
要捕获选项,您需要使用 loader-utils。它用来解析 loader 选项和查询参数。安装它:
npm install loader-utils --save-dev
把它引入到 loader,将其设置为捕获 name
并把 name
传递给 webpack 的插补器:
loaders/demo-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(content) {
const { name } = loaderUtils.getOptions(this);
// const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
// content,
// });
const url = loaderUtils.interpolateName(this, name, { content });
...
};
运行(node ./run-loader.js
)之后,你应该看到一些东西:
{
result: [ 'export default __webpack_public_path__+"demo.txt";' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: []
}
您可以看到结果与 loader 应返回的内容相匹配。您可以尝试将更多选项传递给 loader 或使用查询参数来查看不同组合会发生什么变化。
如果选项不是您所期望的,那么提前验证选项是一个好主意,而不是默默地直接失败。schema-utils 是为此目的而设计的。
将自定义 loader 引入 Webpack
要充分利用 loader,必须将它们与 Webpack 连接起来。要实现这一目标,您可以通过导入:
src/component.js
import "!../loaders/demo-loader?name=foo!./main.css";
鉴于定义冗长,loader 还可以使用别名:
webpack.config.js
const commonConfig = merge([
{
...
resolveLoader: {
alias: {
"demo-loader": path.resolve(
__dirname,
"loaders/demo-loader.js"
),
},
},
},
...
]);
通过此更改,可以简化导入:
// import "!../loaders/demo-loader?name=foo!./main.css";
import "!demo-loader?name=foo!./main.css";
您还可以通过 rules
来处理 loader 的定义。一旦 loader 足够稳定,就可以建立一个基于 webpack-defaults 的项目,将 loader 代码推送到那里,并开始将 loader 作为包使用。
虽然使用 loader-runner 可以方便地开发和测试 loader,但是针对 Webpack 运行的集成测试还是必要的。环境之间的微妙差异使这一点至关重要。
loader 抛掷
Webpack 分两个阶段处理 loader:抛掷和执行。如果您了解 Web 事件,则可以将 loader 的处理过程比作事件的捕获和冒泡。其核心思想是 Webpack 允许您在抛掷(捕获)阶段拦截执行。Webpack 首先从左到右穿过 loader,然后从右到左执行。
我们可以调整一个正在抛掷的 loader 的请求信息,甚至终止它。按照如下设置:
loaders/pitch-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(input) {
const { text } = loaderUtils.getOptions(this);
return input + text;
};
module.exports.pitch = function(remainingReq, precedingReq, input) {
console.log(`
Remaining request: ${remainingReq}
Preceding request: ${precedingReq}
Input: ${JSON.stringify(input, null, 2)}
`);
return "pitched";
};
要把它引入到 runner,请将其添加到 loader 程序的定义中:
run-loader.js
runLoaders(
{
resource: "./demo.txt",
loaders: [
...
path.resolve(__dirname, "./loaders/pitch-loader"),
],
...
},
(err, result) => (err ? console.error(err) : console.log(result))
);
如果你现在运行(node ./run-loader.js
),pitch-loader 应记录中间数据并拦截执行:
Remaining request: ./demo.txt
Preceding request: .../webpack-demo/loaders/demo-loader?{"name":"demo.[ext]"}
Input: {}
{
result: [ 'export default __webpack_public_path__ + "demo.txt";' ],
resourceBuffer: null,
cacheable: true,
fileDependencies: [],
contextDependencies: []
}
loader 缓存
虽然 Webpack 默认缓存加载器,除非设置 this.cacheable(false)
,但编写缓存 loader 可能是一个很好的练习,因为它可以帮助您了解 loader 是如何协同工作的。以下示例显示了如何实现这一目标(由 Vladimir Grenaderov 提供):
const cache = new Map();
module.exports = function(content) {
// Calls only once for given resourcePath
const callbacks = cache.get(this.resourcePath);
callbacks.forEach(callback => callback(null, content));
cache.set(this.resourcePath, content);
return content;
};
module.exports.pitch = function() {
if (cache.has(this.resourcePath)) {
const item = cache.get(this.resourcePath);
if (item instanceof Array) {
// Load to cache
item.push(this.async());
} else {
// Hit cache
return item;
}
} else {
// Missed cache
cache.set(this.resourcePath, []);
}
};
可以使用 pitch-loader 将元数据附加到输入以供稍后使用。在该示例中,在抛掷阶段期间构建了高速缓存,并且在正常执行期间访问了高速缓存。
在官方文档详细地介绍了 loader API。您可以看到
this
中所有可用的字段。
总结
编写 loader 很有趣,因为它们描述了从一种格式到另一种格式的转换。通常,您可以通过研究 API 文档或现有的 loader 来弄清楚如何实现特定的功能。
回顾一下:
- loader-runner 是了解 loader 如何工作的宝贵工具,可以用它来调试 loader。
- Webpack loader 接受输入并基于它生成输出。
- loader 可以是同步的也可以是异步的。在后一种情况下,您应该使用 Webpack
this.async()
API来捕获 Webpack 暴露的回调函数。 - 如果你想为 Webpack 入口动态生成代码,那么 loader 可以派上用场。在这种情况下,loader 可以不必接收输入,它可以只返回输出。
- 使用 loader-utils 解析传递给 loader 的选项,并考虑使用 schema-utils 验证它们。
- 在本地开发 loader 时,请考虑设置
resolveLoader.alias
来重命名引用。 - loader 抛掷阶段补充了默认行为,允许您拦截和附加元数据。
您将在下一章学习编写插件。插件允许您拦截 webpack的执行过程,并且它们可以与加载器组合以开发更高级的功能。