Webpack自定义Loader和Plugin

·  阅读 933

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

Loader

loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。该函数不能是箭头函数,因为函数中的 this 作为上下文会被 webpack 填充,并且 loader runner 中包含一些实用的方法。

同一类型文件可以有多个loader,而起始loader只有一个入参:资源文件的内容。compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为 String 或者 Buffer(能够被转换为 string)类型,代表了模块的 JavaScript 源码。

举个例子,先编写如下demo:

// index.js
console.log('xiong ling');
// webpack.config.js
const path = require('path');
const webpack = require("webpack");

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js',
    },
    devtool: 'inline-source-map',
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true,
    },
    resolveLoader: {
        modules: ['node_modules', './src/loaders']
    },
    module: {
        rules: [
            {
                test: /.js$/i,
                use: ['loader'],
            }
        ]
    }
};

resolveLoader的意思是告诉webpack去哪里找loader,从左到右执行,我们项目中肯定不止我们自定义的loader,所以需要加入node_modules,在遇到.js文件结尾的时候,就用我们自定义的loader去处理,在./src/loaders下去找。因此我们还需要见一个./src/loaders/loader.js文件用来编写我们自定义loader的代码

// ./src/loaders/loader.js
module.exports = function (source) {
    return source.replace('xiong ling', "hello world");
}

在这个函数里面,我们接受了一个source参数,它就是我们的源代码(String),然后我们进行替换,就这么简单,这是同步的loader。我们运行npm run build 试试,我们的内容被成功替换了。

我们知道,在webpack配置中,我们可以传入options配置,传入一些参数到我们的loader中,那我们怎么接收呢?再改一下例子,修改webpack.config.js的配置如下

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /.js$/i,
                use: [
                    {
                        loader: 'loader',
                        options: {
                            name: "hello world --- options"
                        }
                    }
                ],
            }
        ]
    }
};

我们在这加入了options参数,怎么在loader中拿到呢。

// ./src/loaders/loader.js
module.exports = function (source) {
    const options = this.getOptions();
    return source.replace('xiong ling', options.name);
}

从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。之前使用loader-utils这个库的。再次打包,我们的代码也会被修改成console.log('hello world --- options -- resolveLoader');

如果我们有些异步操作呢,该怎么办?还是直接return嘛?当然不是,那样会直接报错,说loader没有返回东西,大家可以尝试一下。现在我们继续修改代码

// ./src/loaders/loader.js
module.exports = function (source) {
    const options = this.getOptions();
    const callback = this.async()

    setTimeout(() => {
        const result = source.replace('xiong ling', options.name);
        callback(null, result)
        // return source.replace('xiong ling', options.name);
    }, 1000)
}

我们不能直接在setTimeout中return,会报错,所以我们需要使用async函数,他告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。webpack就会等待定时器执行完成,然后调用callback函数将结果返回。

至此,我们简单的loader就已经实现了,他还可以用作国际化等操作。接下来来看看webpack中另一个重要概念plugin怎么实现。

Plugin

插件也是webpack生态中的重要一部分,它提供了一种有力的方式让我们直接触及 webpack 的编译过程(compilation process)。插件能够 hook 到每一个编译(compilation)中发出的关键事件中。 在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力, 并且在合适的时机,还可以访问当前的 compilation 对象。

  • Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例,对象包含了 Webpack 环境所有的的配置信息。
  • Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖),这个对象包含了当前的模块资源、编译生成资源、变化的文件等。

创建插件

webpack插件应该有哪些部分组成呢?我们在使用的时候是怎么使用的呢?是new一个插件对吧new XXXPlugin(),那是不是就说明我们的插件是一个构造函数或者类呢。

  • 一个 JavaScript 命名函数或 JavaScript 类。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • apply中可以绑定一个webpack的时间钩子,然后再钩子中执行我们的需求

这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。

接下来我们可以使用一个例子来写一下我们的plugin。就以fileList.md为例吧,需求就是 在每次webpack打包之后,自动产生一个打包文件清单,实际上就是一个markdown文件,上面记录了打包之后的文件夹dist里所有的文件的一些信息。

分析步骤:

  1. 生成一个markdown文件,这个文件名怎么定义,需不需要传参数进去
  2. 在那个钩子里执行我们的操作
  3. 怎么生成我们的markdown文件
  4. 需要什么样的markdown文件

所以我们的代码可以写成如下:

class FileList {
    static defaultOptions = {
        outputFile: 'assets.md',
    };

    constructor(options = {}) {
        // 可以接收自定义的options,如文件名等,进行合并
        this.options = { ...FileList.defaultOptions, ...options };
    }
    apply(compiler) {
      // 在 emit 钩子里执行,他是异步钩子,所以我们需要使用tapAsync来注册,并且必须调用cb函数
        compiler.hooks.emit.tapAsync('FileList', (compilation, cb) => {
            const fileListName = this.options.outputFile;
            // compilation.assets有我们所有的资源文件
            let len = Object.keys(compilation.assets).length;
            // 
            let content = `# 一共有${len}个文件\n\n`;
            // 遍历资源文件,获取name进行拼接
            for (let filename in compilation.assets) {
                content += `- ${filename}\n`
            }
             // 在compilation.assets这资源对象中新添加一个名为fileListName的文件
            compilation.assets[fileListName] = {
                // 文件内容
                source: function () {
                    return content;
                },
                // 文件的长度
                size: function () {
                    return content.length;
                }
            }
            cb()
        })
    }
}

module.exports = FileList;

至此我们的一个插件就写好了,大家可以测试一下。

补充一点的是,上面提到异步钩子需要使用tapAsync来注册,而同步钩子是需要tap来注册的,并且不需要调用cb函数。钩子是同步还是异步的,可以看webpack的描述。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改