毕业3年,终于会写一个基本的webpack插件了

说到webpack,我认识它好久了,差不多4年的时间,可是它不认识我,这就很尴尬。手写一个自定义的webpack插件,这个思想似乎存在很久了,之前总是断断续续的,直到今天,满打满算,应该是毕业正好3年,我来把这一块的东西给弥补上了。

特别强调:这篇文章只面向初级以下的人,其他段位的大神可自行划走,当然如果你想指点一二,评论区也是有你位置的。

话不多说,直接进入正题。

webpack浅认识

首先我们来回顾一下,webpack plugin是如何使用的:

javascript 复制代码
// webpack配置如下
let HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: path.join(__dirname, 'src/index.js')
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'src/index.html')
        })
    ]
}

从上面我们可以看出,plugins是一个实例化类型的数组,意味着如果我们要写一个自定义的webpack plugin,那么我们导出来的插件应该是一个 function 或者是 class。因为在js里,只有这2个关键字支持跟new一块使用。

我们再回到webpack这个工具本身,我们知道它有非常多的plugin、loader,这些东西都是webpack团队开发的吗?

答案肯定不是,如果跟webpack相关的东西都是这个团队开发的,那岂不是要累死。因此为了让plugin、loader对于webpack来说可插拔,可扩展,这个团队基于tapable实现了这样的一个流程控制。

tapable浅认识

大家可以翻阅一下跟webpack相关的一些源码,在这些源码里,我们会发现大量的类似下面的代码:

javascript 复制代码
compiler.hooks.emit.tap('xxx', () => {});
compiler.hooks.done.tapAsync('xxx', () => {});

xxx.tap等这类写法很符合tapable风格,我们再来看看看下面的代码:

javascript 复制代码
let { SyncHook } = require('tapable');

class MyHook{
    constructor(){
        this.hooks = new SyncHook();
    }
    // 注册事件
    registryEvent(eventName, eventFn){
        this.hooks.tap(eventName, () => {
            eventFn();
        });
    }
    // 执行事件
    executeEvent(){
        this.hooks.call();
    }
}

let compiler = new MyHook();

compiler.registryEvent('tapable-1', () => {
    console.log('执行111');
});

complier.registryEvent('tapable-2', () => {
    console.log('执行222');
})

complier.executeEvent();

我们再执行一下这个代码,控制台里面输出了我们打印的信息。

上面的代码非常简单,就是一个 注册事件、触发事件 这样的流程,只不过我们是基于tapable第三方库实现的。

有了对"tapable"这样的认识,再回到webpack上,我们知道webpack的运行是分很多个阶段的,比如初始化阶段(initialize)编译阶段(compilation)产出阶段(emit)完成阶段(done)

我上面说的这几个阶段只是大致的框架,每个框架下面还会细分为很多个小阶段,比如"产出阶段"就分为"静态资源文件生成到指定目录前(emit)"、"静态资源文件生成到指定目录后(afterEmit)"。

每个阶段都是一个tapable对象,这点在webpack源码里就有直接的体现:

每个阶段在执行前以及执行后,都会去调用new SyncHook().call()这样类似的方法,因此我们写webpack plugin的时候,我们需要知道自己的插件是在哪个阶段运行的,这点尤为重要。知道了在哪个阶段运行,那我们就可以在指定的阶段去注册相应的事件。

假如,我现在写一个插件,这个插件的运行时机是在webpack编译完成之后运行,我也不管它编译成功还是失败,根据上图的指示,我们知道,首先要拿到webpack编译对象,然后呢还要拿到编译对象的hooks对象,最后再拿到done对象,在done对象上去注册一个随机事件。当webpack编译结束后,我们的随机事件就会被触发了,代码可能就像下面这样:

javascript 复制代码
compiler.hooks.done.tap('xxx', () => {
    console.log('webpack编译完成');
});

那么问题来了,我们该如何拿到compiler.hooks.done对象呢?

这一点从webpack源码里能够找到答案:

上面的图片告诉我们,如果我们的配置文件里,配置了plugins选项,并且plugins是一个数组,那么就会遍历数组的每一项,并且将compiler对象传入到每一项的apply方法里。

因此当我们在写自定义webpack plugin的时候,我们需要在插件上实现apply方法,然后在apply方法里,我们就能够获取到compiler.hooks对象,我们就可以顺理成章的在指定的阶段去添加监听事件,以此来完成特定的需求。

手写webpack插件

有了上面的基础认知,我们现在来完成一个插件的编写。从上面的信息我们得知,完成一个插件,必须要有3个步骤:

  • 插件必须是一个function或者class对象
  • 插件必须要实现apply方法。
  • 最后将这个插件导出,以供webpack使用。

我们来准备下工程,工程目录如下:

javascript 复制代码
| - project
    | - node_modules
    | - CustomWebpackPlugin
        | - FileListPlugin.js
    | - src
        | - index.js
    | - webpack.config.js
    | - package.json
    | - package.lock.json

我们来安装一下必要的依赖:

javascript 复制代码
// 1、新建project目录
mkdir project
// 2、在这个目录下进行初始化的操作
npm init -y
// 3、安装必要依赖
npm install webpack --save-dev

配置webpack.config.js文件

javascript 复制代码
let webpack = require('webpack');
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let FileListPlugin = require('./CustomWebpackPlugin/FileListPlugin');

module.exports = {
    mode: 'production',
    entry: {
        index: path.join(__dirname, 'src/index.js')
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
        clean: true
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'src/index.html')
        }),
        new FileListPlugin('readme.md')
    ]
}

添加项目启动命令(修改package.json文件):

json 复制代码
// 其余字段都不变
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack --config webpack.config.js"
  },

接下来就是编写FileListPlugin插件,代码如下:

javascript 复制代码
class FileListPlugin {
    constructor(fileName1){
        this.fileName = fileName1;
    }
    apply(compiler){
        let self = this;
        const { webpack } = compiler;

        // Compilation 对象提供了对一些有用常量的访问。
        const { Compilation } = webpack;

        // RawSource 是其中一种 "源码"("sources") 类型,
        // 用来在 compilation 中表示资源的源码
        const { RawSource } = webpack.sources;

        compiler.hooks.thisCompilation.tap('fileListDone', (compilation) => {
            compilation.hooks.processAssets.tap(
                {
                    name: self.fileName,
          
                    // 用某个靠后的资源处理阶段,
                    // 确保所有资源已被插件添加到 compilation
                    stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
                },
                (assets) => {
                    // "assets" 是一个包含 compilation 中所有资源(assets)的对象。
                    // 该对象的键是资源的路径,
                    // 值是文件的源码

                    // 生成 Markdown 文件的内容
                    const content = '# 这是一级标题';
            
                    // 向 compilation 添加新的资源,
                    // 这样 webpack 就会自动生成并输出到 output 目录
                    compilation.emitAsset(
                        self.fileName,
                        new RawSource(content)
                    );
                }
            )
        });

    }
}

module.exports = FileListPlugin;

上面代码是webpack5官方给出的例子,webpack4当时给出的例子可不是这样。上面出现了2个钩子,一个是thisCompilation,它代表着开始编译的阶段;另一个是processAssets,它代表着所有的静态资源已经生成完毕。

这么多钩子函数我们是否都要记住?答案肯定是否的,但是你要熟悉这些钩子函数,只有熟悉了他们,你才能够让自己的插件正常的运行。

当我们运行项目的时候,就会发现在dist目录里能够再生成一个readme文件,这个是符合预期的。

最后

好啦小伙伴,本期分享到这里就结束啦,希望我的分享能够对你有帮助,我们下期再见啦,拜拜~~

相关推荐
_斯洛伐克38 分钟前
下降npm版本
前端·vue.js
苏十八2 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月2 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容3 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德3 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天4 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长4 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L5 小时前
Web基础与HTTP协议
前端·http·php
Amore05255 小时前
React+TS前台项目实战(二十三)-- 基于属性自定义数值显示组件Decimal封装
前端·react.js·typescript·前端框架
friklogff5 小时前
【JavaScript脚本宇宙】美化网格布局:Isotope和Masonry让你的网页焕然一新
开发语言·前端·javascript