Webpack5原理-实现一个Loader
前言
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类型
同步 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);
});
};
this.callback
可以同步或者异步调用的并返回多个结果的函数。预期的参数是:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
第一个参数必须是 Error 或者 null
第二个参数是一个 string 或者 Buffer。
可选的:第三个参数必须是一个可以被 this module 解析的 source map。
可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
loader 最初被设计为可以在同步 loader pipelines(如 Node.js ,使用 enhanced-require),以及 在异步 pipelines(如 webpack)中运行。然而,由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,我们建议尽可能地使你的 loader 异步化。但如果计算量很小,同步 loader 也是可以的。
二、”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;
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
'url-loader',
'raw-test-loader',// 自己的loader
]
}
]
}
}
三、Pitching Loader
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch
方法。loader 可以通过 request 添加或者禁用内联前缀,这将影响到 pitch 和执行的顺序。
对于以下 use
配置:
// webpack.config.js
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
可以访问的一些方法或属性。Webpack5 提供了31个方法或属性,大家需要了解去官方查看就可以。
下面提供一个例子,将使用 require 进行调用:
在 /abc/file.js
中:
require('./loader1?xyz!loader2!./resource?rrr');
五、写一个简单版的Loader
// toggle-case-loader.js
const loaderUtils = require('loader-utils');
const Uppercase2Lowercase = 'UtoL';
const Lowercase2Uppercase = 'LtoU';
module.exports = function (source, map, meta) {
let output = '';
// 获取options
const options = loaderUtils.getOptions(this);
const { formatType } = options;
switch(formatType) {
case Lowercase2Uppercase: {
output = source.toUpperCase();
break;
}
case Uppercase2Lowercase: {
output = source.toLowerCase();
break;
}
default: {
output = source;
}
}
this.callback(null, output, map, meta);
};
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
exclude: /\.(css|js|html|png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'asset',
}
},
{
loader: 'toggle-case-loader',
options: {
formatType: 'LtoU'
}
},
]
}
]
},
// 解析loader包是设置模块如何被解析
resolveLoader: {
modules: ['./node_modules', './loader'],// 告诉 webpack 解析loader时应该搜索的目录。
},
}
总结
- loader 的本质是一个 node 模块,这个模块导出一个函数,这个函数上可能还有一个 pitch 方法。
- 了解了 loader 的本质和 loader 链的执行机制,其实就已经具备了 loader 开发基础了。
- 开发 loader 不难上手,但是要开发一款高质量的 loader,仍需不断实践。
- 尝试自己开发维护一个小 loader 吧~ 没准以后可以通过自己编写 loader 来解决项目中的一些实际问题。