Loader 是 Webpack 中非常重要的组成部分,前面章节中已经介绍了很多 loader 的用法。本章对 loader 的细节做一些详细说明。
借助于 loader-runner 来单独运行一个 loader
我们可以通过 loader-runneropen in new window 实现不借助于 Webpack 直接运行一个 loader。首先安装 loader-runner.
npm add loader-runner --develop
我们先写一个简单的 loader,这个 loader 将输入的文本重复一遍再输出。
loaders/demo-loader.js
module.exports = (input) => input + input;
我们在项目根目录下准备一个 demo.txt
文件,里面写入一些文本。
现在,我们来运行 loader-runner。
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))
);
2
3
4
5
6
7
8
9
10
11
12
此时,执行 node ./run-loader.js
,可以看到输出内容为:
{
result: [ 'foobar\nfoobar\n' ],
resourceBuffer: <Buffer 66 6f 6f 62 61 72 0a>,
cacheable: true,
fileDependencies: [ './demo.txt' ],
contextDependencies: [],
missingDependencies: []
}
2
3
4
5
6
7
8
我们可以看到输出的文本内容和一些元数据信息。
如果我们的 loader 是安装在项目本地,我们可以直接通过 loader 的名字来查找 loader,
loaders: ["babel-loader"]
。
实现一个异步 loader
上面的例子中,我们同步实现了一些功能,但是在某些情况下,我们需要进行一些异步处理,这时候,我们可以借助于 Webpack 提供的异步 API this.async()
。函数返回一个回调函数,与 NodeJS 的回调函数签名一致。
loaders/demo-loader.js
module.exports = function (input) {
const callback = this.async();
callback(null, input + input);
};
2
3
4
5
注意,我们使用的
this
,因此无法使用箭头函数。
此时,执行 node ./run-loader.js
,我们看到同样的输出内容。
如果我们需要抛出错误,则可以这么实现。
loaders/demo-loader.js
module.exports = function (input) {
const callback = this.async();
callback(new Error('Demo error'));
};
2
3
4
5
此时,运行结果将会是一个错误信息。
只返回输出结果
我们可以在 loader 中只返回代码,比如
loaders/demo-loader.js
module.exports = () => 'foobar';
这种做法在需要动态生成代码的时候将会非常有用。
写文件
像 file-loader 之类的 loader 会输出文件。Webpack 提供了 this.emitFile
API。因为 loader-runner 没有实现这个 API,因此我们需要 mock 掉。
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))
);
2
3
4
5
6
7
8
9
10
11
作为一个处理资源的 loader,核心任务就是输出资源内容和文件路径。
我们可以借助于 loader-utilsopen in new window 来对文件名做一些定制处理。
npm add loader-utils --develop
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}`;
};
2
3
4
5
6
7
8
9
10
11
12
13
Webpack 还提供了 this.emitWarning(<string>)
和 this.emitError(<string>)
方法,我们同样需要在 loader-runder 中 mock。
loader-utils 包含了许多实用方法。比如
loaderUtils.parseQuery(this.resourceQuery)
可以处理传递给 loader 的查询参数。
下一步就是如何将文件名传递给下一个 loader。
给 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))
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在 demo loader 中可以使用这个 name
参数。
loaders/demo-loader.js
const loaderUtils = require("loader-utils");
module.exports = function(content) {
// const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
// content,
// });
const { name } = loaderUtils.getOptions(this);
const url = loaderUtils.interpolateName(this, name, { content });
...
};
2
3
4
5
6
7
8
9
10
11
12
13
执行 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: [],
missingDependencies: []
}
2
3
4
5
6
7
8
我们可以在使用参数之前对参数进行一些校验,这是可以使用 schema-utilsopen in new window。
在 Webpack 中使用自定义 loader
src/component.js
import '!../loaders/demo-loader?name=foo!./main.css';
上面的使用方式非常繁琐,我们可以在 webpack 的配置文件中给 loader 定义一个别名,
webpack.config.js
const commonConfig = merge([
{
resolveLoader: {
alias: {
"demo-loader": path.resolve(
__dirname,
"loaders/demo-loader.js"
),
},
},
},
...
]);
2
3
4
5
6
7
8
9
10
11
12
13
这时候,我们可以这样引用这个 loader
// import "!../loaders/demo-loader?name=foo!./main.css";
import '!demo-loader?name=foo!./main.css';
2
我们也可以通过 Webpack 的rule
来引用 loader。
loader 的执行过程
webpack 中 loader 的执行过程。
像浏览器中的事件一样,Webpack 中 loader 的执行过程也分两个阶段。我们可以在 pitch 的过程中做一些定制处理逻辑。Webpack 会从左到右解析 loader,然后从右到左执行。
我们可以定义一个 pitch-loader 来定制 loader 的解析过程,我们设置可以终止 loader 的解析。
loaders/pitch-loader.js
const loaderUtils = require('loader-utils');
module.exports = function (input) {
return input + loaderUtils.getOptions(this).text;
};
module.exports.pitch = function (remaining, preceding, input) {
console.log(`Remaining: ${remaining}, preceding: ${preceding}
Input: ${JSON.stringify(input, null, 2)}
`);
return 'pitched';
};
2
3
4
5
6
7
8
9
10
11
12
每个 loader 都可以提供一个
pitch
函数给 Webpack 调用,在pitch
函数里做一些定制处理。
修改 run-loader,如下
run-loader.js
runLoaders(
{
resource: "./demo.txt",
loaders: [
...
path.resolve(__dirname, "./loaders/pitch-loader"),
],
...
},
(err, result) => (err ? console.error(err) : console.log(result))
);
2
3
4
5
6
7
8
9
10
11
执行 node ./run-loader.js
可以看到输出的一些定制处理信息。
在 loader 中使用缓存
默认情况下 Webpack 都会开启 loader 的缓存,除非我们通过 this.cacheable(false)
来禁用。我们可以通过下面的代码来了解 loader 的缓存是如何工作的。
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) {
item.push(this.async()); // 加载到缓存中
} else {
return item; // 命中缓存
}
} else {
cache.set(this.resourcePath, []); // 未命中缓存
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pitch loader 可以被用来添加一些额外的元数据信息。本例中,在 pitch 阶段,构建缓存,然后在执行阶段使用缓存。
总结
loader-runner 可以帮助我们理解 loader 是如何工作的。
在 Webpack 中,loader 既可以是同步的,也可以是异步的。Webpack 提供了 this.async()
来实现异步 loader。
我们可以通过 loader 实现动态生成代码。
可以使用 loader-utils 来处理参数,同时使用 schema-utils 来对参数进行校验。
在本地开发的时候,我们可以借助于 resolveLoader.alias
的形式避免不必要的路径引用。
loader 的 pitch 阶段可以方便我们添加一些元数据和做 loader 的解决做一些定制化操作。
关注微信公众号,获取最新推送~
加微信,深入交流~