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

相关推荐
Myli_ing4 分钟前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风7 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟16 分钟前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾37 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
疯狂的沙粒2 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript