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源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
我要洋人死20 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人31 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人32 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR37 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香39 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969342 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
阑梦清川1 小时前
在鱼皮的模拟面试里面学习有感
学习·面试·职场和发展
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍