webpack中的plugin是什么?实现一个简易的插件

一. 概念

在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。

一个简单的 plugin 是这样的:

js 复制代码
class Plugin{
      // 注册插件时,会调用 apply 方法
      // apply 方法接收 compiler 对象
      // 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
      apply(compiler){
              // compilation 是监听每次编译循环
              // 每次文件变化,都会生成新的 compilation 对象并触发该事件
        compiler.plugin('compilation',function(compilation) {})
      }
}

注册插件:

js 复制代码
// webpack.config.js
module.export = {
    plugins:[
            new Plugin(options),
    ]
}

二. 事件流机制

Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。

  • 通过链式调用,按顺序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 可以插入到整个生产过程中的每个步骤中;

Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:

js 复制代码
const { SyncHook } = require("tapable")

const hook = new SyncHook(['arg'])
// 订阅
hook.tap('event', (arg) => {
        // 'event-hook'
        console.log(arg)
})

// 广播

hook.call('event-hook')

Webpack 中两个最重要的类 Compiler 与 Compilation 便是继承于 Tapable,也拥有这样的事件流机制。

  • Compiler : 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;

  • Compilation : 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;

  • 区别:

    • Compiler 全局唯一,且从启动生存到结束;
    • Compilation对应每次编译,每轮编译循环均会重新创建;

三. 常用 Plugin:

  • UglifyJsPlugin: 压缩、混淆代码;
  • CommonsChunkPlugin: 代码分割;
  • ProvidePlugin: 自动加载模块;
  • html-webpack-plugin: 自动创建一个HTML文件,并把打包好的JS插入到HTML文件中
  • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
  • optimize-css-assets-webpack-plugin: CSS 代码去重;
  • webpack-bundle-analyzer: 代码分析;
  • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
  • happypack: 使用多进程,加速代码构建;
  • EnvironmentPlugin: 定义环境变量;
  • clean-webpack-plugin 在每一次打包之前,删除整个输出文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离CSS代码,放到一个单独的文件中
  • optimize-css-assets-plugin 压缩css

一. tapable库中的钩子

js 复制代码
const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
        SyncLoopHook,
        AsyncParallelHook,
        AsyncParallelBailHook,
        AsyncSeriesHook,
        AsyncSeriesBailHook,
        AsyncSeriesWaterfallHook
 } = require("tapable");

tapable相关钩子文档链接

同步

  • SyncHook

同步hooks,任务回依次执行

js 复制代码
const { SyncHook, SyncBailHook } = require('tapable')

class Lesson {
    constructor(){
        // 初始化hooks容器
        this.hooks = {
            go: new SyncHook(['address'])
        }
    }

    tap(){

        // 往hooks容器中注册事件/添加回调函数
        this.hooks.go.tap('class1', address=>{
             console.log('class1', address)
             return address
         })
         this.hooks.go.tap('class2', address=>{
             console.log('class2', address)
         })
    }

    start(){
        // 触发hooks
        this.hooks.go.call('打印SyncHook')
    }

}

const item = new Lesson()
item.tap()
item.start()

执行结果:

  • SyncBailHook

一旦有返回值就会退出

js 复制代码
 constructor(){
    // 初始化hooks容器
    this.hooks = {
        go: new SyncBailHook(['address'])
    }
}

执行结果为:

异步

  • AsyncParallelHook

异步并行

js 复制代码
const { AsyncParallelHook, AsyncSeriesHook } = require('tapable')


class Lesson {
    constructor(){
        this.hooks = {
            // 异步并行
            leave: new AsyncParallelHook(['name', 'age'])
        }
    }

    tap(){
        // 异步绑定方式
        this.hooks.leave.tapAsync('class3', (name, age, cb)=>{
            setTimeout(()=>{
                console.log('class3', name, age)
                cb()
            }, 2000)
        })

        // 以promise的形式进行绑定
        this.hooks.leave.tapPromise('class4', (name, age)=>{
            return new Promise(resolve=>{
                setTimeout(() => {
                    console.log('class4', name, age)
                    resolve()
                }, 1000);
            })
        })

    }

    start(){

        this.hooks.leave.callAsync('boll', 24, function(){
            console.log('end~~')
        })
    }
}

const item = new Lesson()
item.tap()
item.start()

执行结果:

  • AsyncSeriesHook

异步串行

js 复制代码
constructor() {
    // 初始化hooks容器
    this.hooks = {
      // AsyncSeriesHook: 异步串行
      leave: new AsyncSeriesHook(['name', 'age'])
    }
  }

执行结果:

按照异步的书写顺序进行执行,不按照设置时间

二. compiler钩子

Compiler 模块是 webpack 的主要引擎,它通过 CLI 传递的所有选项, 或者 Node API,创建出一个 compilation 实例。 它扩展(extend)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。

compile的内部实现

js 复制代码
class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      /** @type {SyncBailHook<Compilation>} */
      shouldEmit: new SyncBailHook(["compilation"]),
      /** @type {AsyncSeriesHook<Stats>} */
      done: new AsyncSeriesHook(["stats"]),
      /** @type {AsyncSeriesHook<>} */
      additionalPass: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook<Compiler>} */

      ......

      ......

      some code

    };

    ......

    ......

    some code
}

可以看到, Compier继承了Tapable, 并且在实例上绑定了一个hook对象, 使得Compier的实例compier可以像这样使用:

js 复制代码
class Plugins1 {
    apply(complier){
        complier.hooks.emit.tap('Plugins1', compilation=>{
            console.log('emit.tap')
        })

        complier.hooks.emit.tapAsync('Plugins1', (compilation, cb)=>{
            setTimeout(() => {
                console.log('emit.tapAsync')
                cb()
            }, 1000);
        })

        complier.hooks.emit.tapPromise('Plugins1', (compilation)=>{
            return new Promise(resolve=>{
                setTimeout(() => {
                    console.log('emit.tapPromise')
                    resolve()
                }, 1000);
            }, 1000)
        })

        complier.hooks.afterEmit.tap('Plugins1', compilation=>{
            console.log('emit.tap')
        })

        complier.hooks.done.tap('Plugins1', complier=>{
            console.log('emit.tap')
        })
    }
}

module.exports = Plugins1

执行结果:

compier常用事件钩子

三. compilation

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。

compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

添加文件plugins

创建plugins2文件:

js 复制代码
class Plugins2 {
    apply(compiler){
        compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
            compilation.hooks.additionalAssets.tapAsync('Plugins2', (cb)=>{
                cb()
            })
        })
    }
}

module.exports = Plugins2
  • 方式一

直接设置静态文件

js 复制代码
class Plugins2 {
    apply(compiler){
        // 初始化compilation钩子
        compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
            // 添加资源
            compilation.hooks.additionalAssets.tapAsync('Plugins2', (cb)=>{
                const content = 'hello burvs'
                // 往要输出资源中,添加一个a.txt
                compilation.assets['a.txt'] = {
                    // 文件大小
                    size(){
                        return content.length
                    },
                    // 文件内容
                    source(){
                        return content
                    }
                }

                cb()
            })
        })
    }
}

module.exports = Plugins2
  • 方式二

使用compilation.assets和fs.readFile读取文件

读取动态设置的文件

js 复制代码
const fs = require('fs')
const path = require('path')
const util = require('util')


const webpack = require('webpack')
const { RawSource } = webpack.sources
// 将fs.readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile)

class Plugins2 {
    apply(compiler){
        compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
            compilation.hooks.additionalAssets.tapAsync('Plugins2', async (cb)=>{

                const data = await readFile(path.resolve(__dirname, 'b.txt'))
                compilation.assets['b.txt'] = new RawSource(data)
                // 方式三:使用compilation.emitAsset方法
                compilation.emitAsset('b.txt', new RawSource(data));
                cb()
            })
        })
    }
}

module.exports = Plugins2

compilation常用事件钩子

事件钩子 触发时机 参数 类型
normal-module-loader 普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。 loaderContext module SyncHook
seal 编译(compilation)停止接收新模块时触发。 - SyncHook
optimize 优化阶段开始时触发。 - SyncHook
optimize-modules 模块的优化 modules SyncBailHook
optimize-chunks 优化 chunk chunks SyncBailHook
additional-assets 为编译(compilation)创建附加资源(asset)。 - AsyncSeriesHook
optimize-chunk-assets 优化所有 chunk 资源(asset)。 chunks AsyncSeriesHook
optimize-assets 优化存储在 compilation.assets 中的所有资源(asset) assets AsyncSeriesHook

四. 实现plugin

CopyWebpackPlugin实现拷贝目录文件。

  1. webpack.config.js文件:
js 复制代码
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')

module.exports = {
  plugins: [
      new CopyWebpackPlugin({
      // 从哪个文件下拷贝
      from: 'public',
      // 拷贝到哪里去
      to: 'css',
      // 忽略那些文件的拷贝
      ignore: ['**/index.html']
    })
  ]
}

schema.json文件:(用于制定options参数规则)

js 复制代码
{
  "type": "object",
  "properties": {
    "from": {
      "type": "string"
    },
    "to": {
      "type": "string"
    },
    "ignore": {
      "type": "array"
    }
  },
  "additionalProperties": false
}

CopyWebpackPlugin.js文件:

js 复制代码
const path = require('path');
const fs = require('fs');
const {promisify} = require('util')

const { validate } = require('schema-utils');
const globby = require('globby');
const webpack = require('webpack');

const schema = require('./schema.json');
const { Compilation } = require('webpack');


const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources

class CopyWebpackPlugin {
  constructor(options = {}) {
    // 验证options是否符合规范
    validate(schema, options, {
      name: 'CopyWebpackPlugin'
    })

    this.options = options;
  }

  apply(compiler) {
    // 初始化compilation
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
      // 添加资源的hooks
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
        // 将from中的资源复制到to中,输出出去
        const { from, ignore } = this.options;
        const to = this.options.to ? this.options.to : '.';

        // context就是webpack配置
        // 运行指令的目录
        const context = compiler.options.context; // process.cwd()
        // 将输入路径变成绝对路径
        const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);

        // 1. 过滤掉ignore的文件
        // globby(要处理的文件夹,options)
        const paths = await globby(absoluteFrom, { ignore });

        console.log(paths); // 所有要加载的文件路径数组

        // 2. 读取paths中所有资源
        const files = await Promise.all(
          paths.map(async (absolutePath) => {
            // 读取文件
            const data = await readFile(absolutePath);
            // basename得到最后的文件名称
            const relativePath = path.basename(absolutePath);
            // 和to属性结合
            // 没有to --> reset.css
            // 有to --> css/reset.css
            const filename = path.join(to, relativePath);
            
            return {
              // 文件数据
              data,
              // 文件名称
              filename
            }
          })
        )

        // 3. 生成webpack格式的资源
        const assets = files.map((file) => {
          const source = new RawSource(file.data);
          return {
            source,
            filename: file.filename
          }
        })

        // 4. 添加compilation中,输出出去
        assets.forEach((asset) => {
          compilation.emitAsset(asset.filename, asset.source);
        })

        cb();
      })
    })
  }
}

module.exports = CopyWebpackPlugin;

输出前public目录:

输出目录:

  • package.json文件
json 复制代码
{
  "name": "04.plugin",
  "version": "1.0.0",
  "description": "",
  "main": "tapable.test.js",
  "scripts": {
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "globby": "^11.0.1",
    "schema-utils": "^3.0.0",
    "tapable": "^2.0.0",
    "webpack": "^5.1.3",
    "webpack-cli": "^4.0.0"
  }
}

写在最后 ⛳

未来可能会更新实现mini-react和antd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
仿生狮子1 小时前
OpenAI 攻下千禧年难题之后,开发者似乎无动于衷
前端·数学·vibecoding
夏幻灵4 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
Lvan的前端笔记4 小时前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
三天不学习5 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs194056 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
明月_清风6 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
Csvn7 小时前
框架性能优化
前端
回眸&啤酒鸭9 小时前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
用户69371750013849 小时前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员9 小时前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端