likes
comments
collection
share

简单实现两个 Webpack 自定义插件

作者站长头像
站长
· 阅读数 1

基础理论知识

1、插件的本质是一个函数或一个

2、webpack 在使用插件时,会初始化一个插件实例并调用其原型对象上的 apply 方法,apply 方法接收一个 compiler 参数,下文将详细介绍这个参数。

函数实例:

    function MyPlugin (options) {

    }

    MyPlugin.prototype.apply = compiler => {

    };

    module.exports = MyPlugin;

类实例:

    class MyPlugin {
        constructor (options) {

        }

        apply (compiler) {

        }
    }

    module.exports = MyPlugin;

compiler 和 compilation

compiler

上文提到的 apply 方法中接收的 compiler 对象代表了完整的 webpack 环境配置。

这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。

可以简单地把它理解为 webpack 实例,使用它来访问 webpack 的主环境和配置信息

另外,compiler 对象暴露了很多生命周期的钩子,通过如下方式使用:

    compiler.hooks.someHook.tap("MyPlugin", params => {
        /* ... */
    });

钩子的访问方式并不固定为 tap,这取决于钩子的类型,主要分为同步和异步,访问的方式有:tapAsync, tapPromise 等。

具体参照:# compiler 钩子

compilation

compilation 对象是从 compiler 钩子的回调函数中传递回来的。

compilation 对象代表了一次资源版本构建。每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。

一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

compilation 对象也暴露了很多生命周期的钩子,以供插件做自定义处理时选择使用。访问方式与 compiler 相同,此处不再赘述。

小结

compiler 是一个全局的对象,是一整个构建的过程,可以访问 webpack 的环境和配置。

compilation 是对于某个模块而言的,它可以更加精细地处理各个模块的构建过程。

简单实现自定义插件

官方文档中给了一个 File List Plugin 自定义插件的示例,该插件主要展示了如何获取构建过程中的资源。

具体参照:# 自定义插件示例

除此之外,我们再来实现两个简单的自定义插件。

Watch Plugin

这个插件的作用是:在 webpack 的监视(watch)模式下,输出每次修改变更的资源文件信息

在查阅官方文档后,发现有个 watchRun 的钩子很符合我们的需求:“在监听模式下,一个新的 compilation 触发之后,但在 compilation 实际开始之前执行。”

也就是说,项目发生了改动,会进行一次新的构建,生成一个新的 compilation,并且在这个 compilation 执行之前触发。

watchRun 的访问方式是 tapAsync,所以除了接收 compiler 参数外,还会接收一个回调函数,我们需要在逻辑执行完毕后调用这个回调函数。

代码如下:

/**
 * 在 webpack 的 watch 模式下触发
 */
class WatchPlugin {
    
    apply (webpackCompiler) {
        
        // watchRun - 在监听模式下触发,在一个 compilation 出现后,在 compilation 执行前触发
        webpackCompiler.hooks.watchRun.tapAsync("WatchPlugin", (compiler, callback) => {
            console.log(" 监听到了! ");

            const mtimes = compiler.watchFileSystem.watcher.mtimes;
            if (!mtimes) return;
            // 通过正则处理,避免显示 node_modules 文件夹下依赖的变化
            const mtimesKeys = Object.keys(mtimes).filter(path => !/(node_modules)/.test(path));
            if (mtimesKeys.length) {
                console.log(` 本次改动了 ${mtimesKeys.length} 个文件,路径为:\n `, mtimes);
            }

            callback();
        });

        // watchClose - 在一个监听中的 compilation 结束时触发
        webpackCompiler.hooks.watchClose.tap("WatchPlugin", () => {
            console.log(" 监听结束,再见! ");
        });

    }

}

module.exports = WatchPlugin;

Clean Plugin

模仿 clean-webpack-plugin 实现一个每次构建时将上一次构建结果中不再需要的文件删除。

梳理逻辑

项目文件改动后,构建结果的文件 hash 值会发生变化,此时将旧文件删除;如果没有发生改动,则无需删除。

实现逻辑

1、考虑在什么时机执行逻辑?

要根据 hash 判断文件是否发生了变化,所以要拿到新、旧文件,那么可以在新的一次构建完成后执行,此时可以获取新构建出的文件。

查阅文档后,compiler.done 这个钩子会在 compilation 完成后触发,符合我们的需求。

2、如何获取上一次构建出的旧文件?

先获取 output 的路径,根据路径就可以获取到 output 文件夹下所有的文件了。这个操作要放在构建开始之前。

上文说过 compiler 可以访问 webpack 的环境与配置,因此通过 compiler 可以获取 output 的路径:

apply (compiler) {
    const outputPath = compiler.options.output.path;
}

获取 output 文件夹中的文件,可以通过 fs.readdirSync 方法和 fs.statSync 方法。

前者是用于读取某一路径下所有的文件名,包括文件和文件夹。

后者是用于判断某一路径是文件还是文件夹。

使用这两个方法,我们可以递归获取 output 文件夹以及其子级文件夹下所有的文件

/**
 * 获取文件夹下所有的文件名(包括子级文件夹中的文件)
 * @param {string} dir 文件夹路径
 */
readAllFiles (dir) {
    let fileList = [];

    const files = fs.readdirSync(dir);
    files.forEach(file => {
        const filePath = path.join(dir, file);
        const stat = fs.statSync(filePath);
        if (stat.isDirectory()) {
            fileList = fileList.concat(this.readAllFiles(filePath));
        } else {
            fileList.push(filePath);
        }
    });

    return fileList;
}

3、如何获取新构建出的文件?

compiler.done 这个钩子的回调函数接收一个参数 stat,通过 stat 可以获取到最新构建出的 assets

这几个问题解决后,我们将新、旧文件路径统一化就可以进行对比了。筛选出需要删除的文件,用 fs.unlinkSync 方法就可以直接删除了。

代码如下:

const fs = require("fs");
const path = require("path");

/** 每次编译时删除上一次编译结果中不再需要的文件 */
class CleanPlugin {

    constructor (options) {
        this.options = options;
    }

    apply (compiler) {
        const pluginName = CleanPlugin.name;

        // 编译输出文件的路径,根据此路径可获取对应目录下的所有文件
        const outputPath = compiler.options.output.path;
        const outputPathPrefix = path.basename(outputPath);
        const oldFiles = this.readAllFiles(outputPath);

        // console.log(" old files ", oldFiles);

        // done - 完成新的编译后执行,此时能获取新的输出文件与现有文件进行对比
        compiler.hooks.done.tap(pluginName, stats => {

            // 新的一次编译完成后的输出文件的相对路径
            const newFiles = stats.toJson().assets.map(assets => fs.realpathSync(`${outputPathPrefix}\\${assets.name}`));

            // console.log(" new files ", newFiles);

            // 新旧文件对比,筛选出需要删除的文件
            const removeFiles = [];
            oldFiles.forEach(oldFile => {
                if (newFiles.indexOf(oldFile) === -1) {
                    removeFiles.push(oldFile);
                }
            });

            // 删除文件
            removeFiles.forEach(removeFile => fs.unlinkSync(removeFile));
        });
    }

    /**
     * 获取文件夹下所有的文件名(包括子级文件夹中的文件)
     * @param {string} dir 文件夹路径
     */
    readAllFiles (dir) {
        let fileList = [];

        const files = fs.readdirSync(dir);
        files.forEach(file => {
            const filePath = path.join(dir, file);
            const stat = fs.statSync(filePath);
            if (stat.isDirectory()) {
                fileList = fileList.concat(this.readAllFiles(filePath));
            } else {
                fileList.push(filePath);
            }
        });

        return fileList;
    }

}

module.exports = CleanPlugin;

总结

项目地址:

# Webpack-Plugins

参考文章:

# webpack 中文文档