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
流程的解析。