《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader运行与调试

简介: 《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader运行与调试


loaderwebpack提供解析计算机文件类型能力,但是它是依赖于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-loadercss-loader, less-loaderwebpack启动后会以一种所谓“链式调用”的方式按 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

  • 运行结果

image.png

写了这么多,也知道有一个pitch函数会在loader之前运行,那么这个pitch函数到底有什么作用呢?


picth真说有什么用我不清楚,但是它可以阻塞后续loader的内容解析,只要return出内容就可以了,这个就留作感兴趣的同学自行去尝试吧。


总结


loader作为webpack的文件解析器,自身有着一整套生命周期和解析流程,就算不开发loader,这套思想也可以为我们的日常开发提供一定的参考思路,loader总体还是很复杂的,这一篇我记录的并不是很详细,因为还是有点一知半解,官网也没找到比较好的参考资料,后续会考虑补一个全面对loader流程的解析。


目录
相关文章
|
2月前
|
Web App开发 JSON 前端开发
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
53 0
|
2月前
|
存储 前端开发 JavaScript
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)
50 0
|
3月前
|
JSON 前端开发 JavaScript
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)(上)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
56 0
|
2月前
|
缓存 前端开发 算法
Webpack 进阶:深入理解其工作原理与优化策略
Webpack 进阶:深入理解其工作原理与优化策略
45 2
|
3月前
|
前端开发 JavaScript
webpack 核心武器:loader 和 plugin 的使用指南(下)
webpack 核心武器:loader 和 plugin 的使用指南(下)
webpack 核心武器:loader 和 plugin 的使用指南(下)
|
3月前
|
JSON 前端开发 JavaScript
webpack 核心武器:loader 和 plugin 的使用指南(上)
webpack 核心武器:loader 和 plugin 的使用指南(上)
webpack 核心武器:loader 和 plugin 的使用指南(上)
|
3月前
|
XML JSON 前端开发
说说webpack中常见的loader?解决了什么问题?
在Webpack中,Loader是用于处理各种文件类型的模块加载器,它们用于对文件进行转换、处理和加载。常见的Loader解决了以下问题:
19 0
|
3月前
|
Web App开发 前端开发 JavaScript
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)(下)
Webpack【搭建Webpack环境、Webpack增加配置文件、Webpack中使用Loader、Webpack分离CSS文件 】(一)-全面详解(学习总结---从入门到深化)
26 0
|
3月前
|
存储 前端开发 JavaScript
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)(下)
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)
19 0
|
3月前
|
测试技术 开发工具 开发者
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)(上)
Webpack【Webpack中模式(Mode)、Webpack中使用DevServer、Webpack中devtool增强调试过程】(二)-全面详解(学习总结---从入门到深化)
30 0