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 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也拥有这样的事件流机制。

  • Compiler : 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 optionsloaders, 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 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 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.assetsfs.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-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
玩电脑的辣条哥2 小时前
Python如何播放本地音乐并在web页面播放
开发语言·前端·python
ew452182 小时前
ElementUI表格表头自定义添加checkbox,点击选中样式不生效
前端·javascript·elementui
suibian52352 小时前
AI时代:前端开发的职业发展路径拓宽
前端·人工智能
Moon.92 小时前
el-table的hasChildren不生效?子级没数据还显示箭头号?树形数据无法展开和收缩
前端·vue.js·html
垚垚 Securify 前沿站2 小时前
深入了解 AppScan 工具的使用:筑牢 Web 应用安全防线
运维·前端·网络·安全·web安全·系统安全
工业甲酰苯胺5 小时前
Vue3 基础概念与环境搭建
前端·javascript·vue.js
mosquito_lover16 小时前
怎么把pyqt界面做的像web一样漂亮
前端·python·pyqt
测试涛叔7 小时前
高级自动化测试常见面试题(Web、App、接口)
软件测试·面试
绝无仅有9 小时前
Deepseek 万能提问公式:高效获取精准答案
后端·面试·架构
柴柴的小记9 小时前
前端vue引入特殊字体不生效
前端·javascript·vue.js