概述

plugin相比于loader来说,他的作用会更宽一些,因为loader他只是在加载模块的环节去工作。而插件的作用范围几乎可以触及到webpack工作的每一个环节。

这样的插件机制究竟是如何实现的呢,其实说起来也非常简单。webpack的插件机制其实就是我们再软件开发过程中最长见到的,钩子机制。

那钩子机制也特别容易理解,有点类似我们web当中的事件。那在webpack的工作过程中会有很多的环节。为了便于插件的扩展。webpack几乎给每一个环节都埋下了一个钩子。

那这样的话我们再去开发插件的时候,我们就可以通过往这些不同的节点上面去挂载不同的任务,可以轻松地去扩展webpack的能力。

具体有哪些预先定义好的钩子,我们可以参考官方的API文档,接下来我们来定义一个插件,来看具体如何往这些钩子上去挂载任务。

webpack要求我们的插件必须是一个函数,或者是一个包含apply方法的对象。一般我们都会把这个插件定义为一个类型,然后在这个类型中去定义一个apply方法,我们使用的时候就是通过这个类型去构建一个实例,然后去使用。

所以我们这里定义一个MyPlugin的类型,然后我们在这个类型中去定义一个apply方法,这个方法会在webpack启动时自动被调用。接收一个compiler对象参数。这个对象就是webpack工作过程中最核心的一个对象。这个对象里面包含了我们此次构建的所有的配置信息,我们也是通过这个对象去注册钩子函数。

这里我们的需求是希望这个插件可以用来去清除webpack打包生成的js当中那些没有必要的注释,那这样一来的话我们bundle.js当中去除了这些注释之后就可以更加容易阅读。有了这个需求过后呢我们需要明确我们这个任务的执行时机,也就是我们要把这个任务挂载到哪个钩子上。

我们的需求是删除bundle.js当中的注释,也就是说我们只有当webpack需要生成这个bundle.js文件的内容明确了过后,我们才可以实施相应的动作,那我们回到webpack的官网。我们找到他的API文档。

在API文档当中我们找到一个叫做emit的钩子,根据文档当中的提示我们发现这个钩子在webpack即将要往输出目录输出文件时执行。非常符合我们的需求。

我们回到代码当中,我们通过compiler当中的hooks属性我们去访问到emit钩子。然后我们通过tap方法去注册一个钩子函数。

这个方法接收两个参数,第一个参数是插件的名称。我们这里是MyPlugin,第二个就是我们需要挂载到这个钩子上的函数。这里我们可以在函数中接收一个complation的对象参数,这个对象可以理解成此次打包过程的上下文。

我们所有打包过程中产生的结果都会放到这个对象中。我们这里使用一下这个对象的assets属性。然后去获取我们即将想入到目录文件中的资源信息。complation.assets;

那他是一个对象,我们这里通过for in 去遍历这个对象。那这个对象当中的键就是每一个文件的名称,我们尝试把他打印出来。然后将这个插件我们应用到我们的配置当中。通过 new MyPlugin的方式把他应用起来。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
class MyPlugin {
    apply(compiler) {
        console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                console.log(name);
            }
        })
    }
}

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html'
        }),
        new CopyWebpackPlugin([
            'public/**'
        ]),
        new MyPlugin()
    ]
}

然后回到命令行中再次运行打包。此时我们打包过程中就会输出出来我们打包的文件名称。那我们再回到代码当中我们再来尝试一下打印每一个资源文件的内容。

那文件的内容我们是要通过这个文件当中的值的source方法来去获取。

我们通过assets然后访问到具体属性的值,然后通过source方法拿到他对应的内容。

class MyPlugin {
    apply(compiler) {
        console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                console.log(assets[name].source());
            }
        })
    }
}

然后回到命令行,再次打包。此时呢打包过程中我们输出的文件内容也可以正常被打印。

可以拿到文件名和内容过后呢。我们再回到代码当中。我们要去判断我们的文件是否以.js结尾,因为我们这里的需求只是去处理js文件。所以如果不是js文件我们就不需要去处理他。

这里如果是js文件的话我们将文件的内容得到。然后我们通过正则的方式去替换掉我们代码当中对应的注释,这里需要注意的是我们正则要以全局模式去替换,将这个替换的结果我们需要去覆盖到原有的内容当中,我们要去覆盖complation当中的assets里面所对应的那个属性。

那这个属性的值我们同样去暴露一个source方法用来去返回我们这个新的内容。除此之外我们还需要一个size方法,用来去返回这个内容的大小,这个方法是webpack内部要求的一个必须的方法。

class MyPlugin {
    apply(compiler) {
        // console.log('MyPlugin 启动');

        compiler.hooks.emit.tap('MyPlugin', complation => {
            for (const name in complation.assets) {
                // console.log(assets[name].source());
                if (name.endsWith('.js')) {
                    const contents = complation.assets[name].source();
                    const withoutComments = contents.replace(/\/*\**\*\//g, '');
                    complation.assets[name] = {
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}

完成以后我们再次回到命令行终端,然后运行打包,打包完成过后我们再来看一下,bundle.js,此时bundle.js每一行开头的注释就都被移除掉了。

以上就是我们实现的一个移除webpack注释插件的一个过程,通过这个过程我们了解了,插件是通过往webpack生命周期里面的一些钩子函数里面去挂载我们任务函数来去实现的,当然如果你需要深入去了解插件机制,可能需要去理解一些webpack底层的实现原理,那这些在我们的文档当中其实并没有详细的介绍。所以你需要通过去阅读源代码来了解他们,关于源代码的一些其他信息或者是运行的底层原理,这些我们后面再来介绍。