Loader 是一个导出函数的 Node 模块。当资源需要由该 loader 转换时,会调用此函数。给定函数将通过提供给它的 this
上下文访问 Loader API。
在我们深入了解不同类型的 loader、它们的用法和示例之前,让我们看看三种在本地开发和测试 loader 的方法。
要测试单个 loader,你可以在规则对象中使用 path
来 resolve
一个本地文件
webpack.config.js
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {
/* ... */
},
},
],
},
],
},
};
要测试多个 loader,你可以利用 resolveLoader.modules
配置来更新 webpack 搜索 loader 的位置。例如,如果你的项目中有本地 /loaders
目录
webpack.config.js
const path = require('path');
module.exports = {
//...
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')],
},
};
顺便说一下,如果你已经为你的 loader 创建了一个单独的仓库和包,你可以通过 npm link
将其链接到你想要测试的项目中。
当单个 loader 应用于资源时,loader 只会带一个参数被调用——一个包含资源文件内容的字符串。
同步 loader 可以 return
一个表示转换后模块的单个值。在更复杂的情况下,loader 可以通过使用 this.callback(err, values...)
函数返回任意数量的值。错误要么传递给 this.callback
函数,要么在同步 loader 中抛出。
loader 预期返回一个或两个值。第一个值是作为字符串或缓冲区的生成 JavaScript 代码。第二个可选值是作为 JavaScript 对象的 SourceMap。
当多个 loader 被链式调用时,重要的是要记住它们以相反的顺序执行——从右到左或从下到上,具体取决于数组格式。
在下面的示例中,foo-loader
将接收原始资源,而 bar-loader
将接收 foo-loader
的输出,并返回最终转换的模块和(如果需要)一个 source map。
webpack.config.js
module.exports = {
//...
module: {
rules: [
{
test: /\.js/,
use: ['bar-loader', 'foo-loader'],
},
],
},
};
编写 loader 时应遵循以下指导原则。它们按重要性排序,有些只适用于特定场景,请阅读后续详细章节以获取更多信息。
Loader 应该只执行一项任务。这不仅使维护每个 loader 的工作变得更容易,而且还允许它们被链式调用,以用于更多场景。
利用 loader 可以链式调用的事实。与其编写一个处理五项任务的单个 loader,不如编写五个更简单的 loader 来分担这项工作。将它们隔离不仅能使每个单独的 loader 保持简单,而且还可能使它们用于你最初未曾想到的目的。
以通过 loader 选项或查询参数指定数据来渲染模板文件为例。它可以编写为一个单独的 loader,该 loader 从源代码编译模板,执行它并返回一个导出包含 HTML 代码字符串的模块。然而,根据指导原则,存在一个可以与其他开源 loader 链式调用的 apply-loader
pug-loader
:将模板转换为导出函数的模块。apply-loader
:使用 loader 选项执行函数并返回原始 HTML。html-loader
:接受 HTML 并输出有效的 JavaScript 模块。保持输出模块化。loader 生成的模块应遵循与普通模块相同的设计原则。
确保 loader 在模块转换之间不保留状态。每次运行都应始终独立于其他已编译模块以及同一模块的先前编译。
利用 loader-utils
包,它提供了各种有用的工具。除了 loader-utils
,还应该使用 schema-utils
包来进行基于 JSON Schema 的 loader 选项的持续验证。下面是一个同时使用两者的简短示例
loader.js
import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string',
},
},
};
export default function (source) {
const options = this.getOptions();
validate(schema, options, {
name: 'Example Loader',
baseDataPath: 'options',
});
console.log('The request path', urlToRequest(this.resourcePath));
// Apply some transformations to the source...
return `export default ${JSON.stringify(source)}`;
}
在 webpack 中,loader 可以链式调用并与链中的后续 loader 共享数据。为此,你可以在原始 loader 中使用 this.callback
方法将数据与内容(源代码)一起传递。在原始 loader 的默认导出函数中,你可以使用 this.callback
的第四个参数传递数据。
export default function (source) {
const options = getOptions(this);
// Pass data using the fourth argument of this.callback
this.callback(null, `export default ${JSON.stringify(source)}`, null, {
some: data,
});
}
在上面的示例中,this.callback
的第四个参数中的 some
属性用于将数据传递给下一个链式 loader。
如果 loader 使用了外部资源(即通过从文件系统读取),它们必须指明。此信息用于在 watch 模式下使可缓存 loader 失效并重新编译。下面是使用 addDependency
方法实现此目的的简短示例
loader.js
import path from 'path';
export default function (source) {
var callback = this.async();
var headerPath = path.resolve('header.js');
this.addDependency(headerPath);
fs.readFile(headerPath, 'utf-8', function (err, header) {
if (err) return callback(err);
callback(null, header + '\n' + source);
});
}
根据模块类型,可能使用不同的 schema 来指定依赖。例如在 CSS 中,使用 @import
和 url(...)
语句。这些依赖应由模块系统解析。
这可以通过以下两种方式之一完成
require
语句。this.resolve
函数解析路径。css-loader
是第一种方法的一个很好的例子。它通过将 @import
语句替换为对其他样式表的 require
,并将 url(...)
替换为对引用文件的 require
,从而将依赖转换为 require
。
对于 less-loader
,它无法将每个 @import
转换为 require
,因为所有 .less
文件都必须一次性编译以进行变量和 mixin 跟踪。因此,less-loader
使用自定义路径解析逻辑扩展了 less 编译器。然后它利用第二种方法,this.resolve
,通过 webpack 解析依赖。
避免在 loader 处理的每个模块中生成通用代码。相反,在 loader 中创建一个运行时文件,并生成一个指向该共享模块的 require
src/loader-runtime.js
const { someOtherModule } = require('./some-other-module');
module.exports = function runtime(params) {
const x = params.y * 2;
return someOtherModule(params, x);
};
src/loader.js
import runtime from './loader-runtime.js';
export default function loader(source) {
// Custom loader logic
return `${runtime({
source,
y: Math.random(),
})}`;
}
不要在模块代码中插入绝对路径,因为当项目根目录移动时,它们会破坏哈希。你可以使用下面的代码将绝对路径转换为相对路径。
// `loaderContext` is same as `this` inside loader function
JSON.stringify(
loaderContext.utils.contextify(
loaderContext.context || loaderContext.rootContext,
request
)
);
如果你正在开发的 loader 只是另一个包的简单封装,那么你应该将该包作为 peerDependency
包含进来。这种方法允许应用程序开发者在需要时在 package.json
中指定确切的版本。
例如,sass-loader
将 node-sass
指定为对等依赖,如下所示
{
"peerDependencies": {
"node-sass": "^4.0.0"
}
}
那么你已经编写了一个 loader,遵循了上述指导原则,并将其设置为在本地运行。接下来是什么?我们来看一个单元测试示例,以确保我们的 loader 按预期工作。我们将使用 Jest 框架来完成此操作。我们还将安装 babel-jest
和一些预设,这些预设将允许我们使用 import
/ export
和 async
/ await
。让我们首先安装并将它们保存为 devDependencies
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
我们的 loader 将处理 .txt
文件,并将 [name]
的任何实例替换为提供给 loader 的 name
选项。然后它将输出一个有效的 JavaScript 模块,其中包含文本作为其默认导出
src/loader.js
export default function loader(source) {
const options = this.getOptions();
source = source.replace(/\[name\]/g, options.name);
return `export default ${JSON.stringify(source)}`;
}
我们将使用此 loader 处理以下文件
test/example.txt
Hey [name]!
请密切注意下一步,因为我们将使用 Node.js API 和 memfs
来执行 webpack。这使我们能够避免将 output
发射到磁盘,并让我们能够访问 stats
数据,我们可以使用这些数据来获取我们转换后的模块
npm install --save-dev webpack memfs
test/compiler.js
import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.txt$/,
use: {
loader: path.resolve(__dirname, '../src/loader.js'),
options,
},
},
],
},
});
compiler.outputFileSystem = createFsFromVolume(new Volume());
compiler.outputFileSystem.join = path.join.bind(path);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
if (stats.hasErrors()) reject(stats.toJson().errors);
resolve(stats);
});
});
};
现在,最后,我们可以编写测试并添加 npm 脚本来运行它
test/loader.test.js
/**
* @jest-environment node
*/
import compiler from './compiler.js';
test('Inserts name and outputs JavaScript', async () => {
const stats = await compiler('example.txt', { name: 'Alice' });
const output = stats.toJson({ source: true }).modules[0].source;
expect(output).toBe('export default "Hey Alice!\\n"');
});
package.json
{
"scripts": {
"test": "jest"
},
"jest": {
"testEnvironment": "node"
}
}
一切就绪,我们可以运行它,看看我们的新 loader 是否通过了测试
PASS test/loader.test.js
✓ Inserts name and outputs JavaScript (229ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.853s, estimated 2s
Ran all test suites.
它奏效了!此时,你已准备好开始开发、测试和部署你自己的 loader。我们希望你能与社区其他人分享你的作品!