毕业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文件,这个是符合预期的。

最后

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

相关推荐
前端大卫1 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘1 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare1 小时前
浅浅看一下设计模式
前端
Lee川1 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix2 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人2 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl2 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端