携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
loader为webpack提供解析计算机文件类型能力,但是它是依赖于webpack运行的,那么它该如何调试?webpack可以同时加载多个loader,它加载的流程又是什么样的?接下来就一起来看看吧。
为 Loader 编写单元测试
任何功能,编写单元测试都是很有必要的,对于不同的应用程序,运行的环境不一样,编写的单元测试代码也不一样,例如webpack的单元测试就肯定是需要webpack的运行环境的,webpack的运行环境很简单,npx webpack不就启动了一个webpack环境,但是单元测试一般都是代码直接运行跑程序,那么如何不通过命令行启动一个webpack环境呢?
1. 在node环境中运行webpack
我们可以直接引入webpack,然后通过node运行webpack,传入对应的webpack配置就ok了:
// 引入webpack
const webpack = require('webpack');
const MemoryFS = require('memory-fs');
module.exports = function () {
// 使用项目中的配置
const webpackConfig = require('../../webpack.config.js');
// 测试环境配置
const config = {
mode: 'development',
devtool: 'sourcemap',
entry: `../main.js`,
output: false
}
// 合并覆盖
Object.assign(webpackConfig, config)
// 获得webpack编译实例
const compiler = webpack(config)
// 将输出内容输出到内存中
compiler.outputFileSystem = new MemoryFS()
// 返回一个promise,最终结果在成功的回调中
return new Promise((resolve, reject) => compiler.run((err, stats) => {
if (err) reject(err)
resolve(stats)
}))
}
使用jest进行测试
// 指向上面的文件
const compiler = require('./compiler.js');
describe('Loader', () => {
test('Defaults', () => {
return compiler()
.then((stats) => {
const [module] = stats.toJson().modules
expect(module.source).toMatchSnapshot()
})
.catch((err) => err)
})
})
2. 使用mock模拟webpack环境
上面的方法和直接使用命令行运行webpack区别不是很大,效率也比较低下,mock环境还是比较推荐的。
// 手写一个构造函数,用来模拟 webpack 上下文,里面只需要模拟自己使用到的属性即可
function WebpackLoaderMock (options) {
this.context = options.context || '';
this.query = options.query;
this.options = options.options || {};
this.resource = options.resource;
this._asyncCallback = options.async;
}
WebpackLoaderMock.prototype.async = function () {
return this._asyncCallback;
};
function testTemplate(loader, testFn) {
loader.call(new WebpackLoaderMock({
async: function (err, source) {
testFn(source);
}
}), '这里是输入loader解析的数据');
}
describe('macro', function () {
it('should be parsed', function (done) {
// 自己的 loader
const loader = require('./loader.js');
testTemplate(loader, function (output) {
// 解析完成会进入这个回调函数,来判断结果是否ok
assert.equal(output, '这里是正确解析结果');
done();
});
});
});
链式调用模型详解
链式调用的意思就是一个文件类型使用到了多个loader处理,那么这些loader是如何协同处理这一个文件,例如.less文件,我们一般需要配置style-loader、css-loader, less-loader,webpack启动后会以一种所谓“链式调用”的方式按 use 数组顺序从后到前调用loader:
module.exports = {
module: {
rules: [
{
test: /.less$/i,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
- 首先调用
less-loader将 Less 代码转译为 CSS 代码; - 将
less-loader结果传入css-loader,进一步将 CSS 内容包装成类似module.exports = "${css}"的 JavaScript 代码片段; - 将
css-loader结果传入style-loader,在运行时调用 injectStyle 等函数,将内容注入到页面的<style>标签。
链式调用分工明确,每一个loader处理完自己的职责之后,将结果丢给下一个loader进行处理。
不过,这只是链式调用的一部分,这里面有两个问题:
- Loader 链条一旦启动之后,需要所有 Loader 都执行完毕才会结束,没有中断的机会 —— 除非显式抛出异常;
- 某些场景下并不需要关心资源的具体内容,但 Loader 需要在 source 内容被读取出来之后才会执行。
为了解决上面这两个问题,webpack引出了pitch函数,在loader上挂载的pitch函数,要比loader提前运行,下面的官网原话:
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata) ,并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的
pitch方法。
官网中有很完整的说明,就是写几个空的loader,挂载几个pitch函数就ok了:pitching-loader
Pitch 函数的完整签名:
/**
* @param remainingRequest 当前 loader 之后的资源请求字符串
* @param previousRequest 在执行当前 loader 之前经历过的 loader 列表
* @param data 与 Loader 函数的 `data` 相同,用于传递需要在 Loader 传播的信息。
*/
function pitch(
remainingRequest: string, previousRequest: string, data = {}
): void {
}
示例代码:
// webpack.config.js
const path = require('path');
module.exports = {
mode: "development",
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[name].js",
clean: true
},
module: {
rules: [{
test: /.js$/,
use: ['./src/loader/a-loader.js', './src/loader/b-loader.js', './src/loader/c-loader.js'], // 这里先后挂载了3个loader
}]
}
}
- a-loader
function loader(source, sourceMap, data) {
console.log('==========> 这里是a-loader 的 loader,我是第六个执行的 <==========');
console.log();
this.callback(
null,
source,
sourceMap,
data
);
}
loader.pitch = function (remainingRequest, previousRequest, data ) {
console.log('==========> 这里是 a-loader 的 pitch,我是第一个执行的 <==========');
console.log('remainingRequest', remainingRequest);
console.log('previousRequest', previousRequest);
console.log('data', data);
console.log();
}
module.exports = loader
- b-loader
function loader(source, sourceMap, data) {
console.log('==========> 这里是b-loader 的 loader,我是第五个执行的 <==========');
console.log();
this.callback(
null,
source,
sourceMap,
data
);
}
loader.pitch = function (remainingRequest, previousRequest, data ) {
console.log('==========> 这里是b-loader 的 pitch,我是第二个执行的 <==========');
console.log('remainingRequest', remainingRequest);
console.log('previousRequest', previousRequest);
console.log('data', data);
console.log();
}
module.exports = loader
- c-loader
function loader(source, sourceMap, data) {
console.log('==========> 这里是c-loader 的 loader,我是第四个执行的 <==========');
console.log();
this.callback(
null,
source,
sourceMap,
data
);
}
loader.pitch = function (remainingRequest, previousRequest, data ) {
console.log('==========> 这里是c-loader 的 pitch,我是第三个执行的 <==========');
console.log('remainingRequest', remainingRequest);
console.log('previousRequest', previousRequest);
console.log('data', data);
console.log();
}
module.exports = loader
- 运行结果
写了这么多,也知道有一个pitch函数会在loader之前运行,那么这个pitch函数到底有什么作用呢?
picth真说有什么用我不清楚,但是它可以阻塞后续loader的内容解析,只要return出内容就可以了,这个就留作感兴趣的同学自行去尝试吧。
总结
loader作为webpack的文件解析器,自身有着一整套生命周期和解析流程,就算不开发loader,这套思想也可以为我们的日常开发提供一定的参考思路,loader总体还是很复杂的,这一篇我记录的并不是很详细,因为还是有点一知半解,官网也没找到比较好的参考资料,后续会考虑补一个全面对loader流程的解析。