webpack plugin 从入门到入门

847 阅读7分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

前言

webpack plugin是webpack的重要组成部分,webpack的功能有80%以上都是使用plugin实现的,本文分上中下三篇,从了解tapable去了解webpack plugin的运作机制,以及webpack plugin的api介绍,以及plugin实践。分别内容如下:

  • 基础
    • webpack plugin 介绍
    • tapable 模块介绍
  • 重点
    • compiler 介绍
    • compilation 介绍
  • 扩展
    • 其他hook API 介绍
    • plugin 实践

webpack plugin

在详细了解webpack插件之前,需要先了解webpack的一些基本工作原理。

依赖分析

webpack主要功能是从入口文件开始读取文件源码,找到它的依赖。然后读取依赖文件,继续找它们的依赖,一直递归下去。

模块映射

在依赖分析期间,webpack会进行模块映射,把分析过的文件内容都放在一个map中(大文件),通过文件路径及文件名相关联的值作为key。

API介绍

在上述过程中,webpack提供了一系列hook及相关对象,让开发者可以在webpack打包的过程中做一些优化处理,或做一些额外的自定义处理。

image.png

  • compiler: 顶级API,提供大部分webpack执行钩子。提供webpack执行参数对象以及配置信息对象。
  • compilation: 通过compiler访问,能获取到编译时的模块信息,依赖信息
  • Resolvers: webpack会把entry的配置路径提供给 Resolver,它会检查给定的部分路径是否存在,并返回完整的绝对路径以及上下文、请求、调用等额外信息。在 compiler 类中,提供了三种类型的内置解析器:
    • normal: 通过绝对或相对路径解析模块。
    • context: 在给定的上下文中解析模块。
    • loader: 解析 webpack loader。
  • NormalModuleFactory 与 ContextModuleFactory Hooks: 工厂创建对象或实例。从入口点开始,它解析每个请求,解析内容以查找进一步的请求,并通过解析所有文件和解析任何新文件来继续爬取文件。在最后阶段,每个依赖项都成为一个 Module 实例。
  • JavascriptParser: 提供模块解释的API,让开发者可以自定义处理模块解释的过程。

Tapable

上文介绍的webpack API都继承了Tapable类。Tapable使用发布订阅模式,封装了一系列API控制钩子函数的发布与订阅。

接下来主要介绍tapable模块下4个类:

  • SyncHook: 同步钩子,任务会从先到后依次逐个执行。
  • SyncBailHook: 确保同步钩子,提供终止机制中断钩子任务执行。
  • AsyncSeriesHook: 异步串行任务钩子。
  • AsyncParallelHook: 异步并行任务钩子。

准备工作

创建一个 tapable_test 目录,进入目录,创建 test.js。

|- tapable_test
    |- test.js

安装 tapable 依赖。

npm i -D tapable
//或
yarn add -D tapable

SyncHook

同步执行钩子,触发所有消费者,消费者回调依次执行。

test.js内容如下:

const { SyncHook } = require('tapable');

class TapableTest {
    constructor() {
        this.hooks = {
            // 实例化同步钩子,赋值给 hooks.syncHook, 接收两个参数。
            syncHook: new SyncHook(['params1', 'params2'])
        }
    }
    // 添加消息消费者
    init() {
        // hooks.syncHook 为 SyncHook类的实例, 使用 tap 方法添加消息的消费者
        this.hooks.syncHook.tap('pluginA', (params1, params2) => {
            console.log(new Date(), 'pluginA', params1, params2)
            return true
        })
        this.hooks.syncHook.tap('pluginB', (params1, params2) => {
            console.log(new Date(), 'pluginB', params1, params2)
            return true
        })
        this.hooks.syncHook.tap('pluginC', (params1,params2) => {
            console.log(new Date(), 'pluginC', params1, params2)
            return true
        })
        return this
    }
    // 调用
    start() {
        // 触发syncHook,并传入参数
        this.hooks.syncHook.call({test: 'hello world'}, 'SyncHook');
    }
}

const test = new TapableTest()

test.init()
test.start()

控制台命令行执行:

node test.js

输出结果:

2022-02-24T14:50:15.255Z pluginA { test: 'hello world' } SyncHook
2022-02-24T14:50:15.262Z pluginB { test: 'hello world' } SyncHook
2022-02-24T14:50:15.262Z pluginC { test: 'hello world' } SyncHook

实例化SyncHook对象后,使用tap方法注册消费者,使用call方法触发所有消费者。从输出结果可以看出消费者按注册顺序依次执行。

SyncBailHook

确保同步钩子,按顺序执行注册的消费者回调,一旦其中一个消费者return返回值,立刻中断后续执行。

test.js内容更新如下:

const { SyncBailHook } = require('tapable');

class TapableTest {
    constructor() {
        this.hooks = {
            // 实例化同步钩子,赋值给 hooks.syncBailHook, 接收两个参数。
            syncBailHook: new SyncBailHook(['params1', 'params2'])
        }
    }
    // 添加消息消费者
    init() {
        // hooks.syncBailHook 为 SyncBailHook类的实例, 使用 tap 方法添加消息的消费者
        this.hooks.syncBailHook.tap('pluginA', (params1, params2) => {
            console.log(new Date(), 'pluginA', params1, params2)
        })
        this.hooks.syncBailHook.tap('pluginB', (params1, params2) => {
            console.log(new Date(), 'pluginB', params1, params2)
            return true
        })
        this.hooks.syncBailHook.tap('pluginC', (params1,params2) => {
            console.log(new Date(), 'pluginC', params1, params2)
        })
        return this
    }
    // 调用
    start() {
        // 触发syncBailHook,并传入参数
        this.hooks.syncBailHook.call({test: 'hello world'}, 'syncBailHook');
    }
}

const test = new TapableTest()

test.init()
test.start()

控制台命令行执行:

node test.js

输出结果:

2022-02-24T15:31:15.835Z pluginA { test: 'hello world' } syncBailHook
2022-02-24T15:31:15.843Z pluginB { test: 'hello world' } syncBailHook

实例化SyncBailHook对象后,使用tap方法注册消费者,使用call方法触发所有消费者。其中pluginB消费者回调函数中return true,中断后续执行。

AsyncSeriesHook

异步串行任务钩子,并行执行注册回调。和同步钩子不一样,需要使用tapAsync或tapPromise注册消费者,callAsync触发消费者。

const { AsyncSeriesHook } = require('tapable');

class TapableTest {
    constructor() {
        this.hooks = {
            // 实例化同步钩子,赋值给 hooks.asyncSeriesHook, 接收两个参数。
            asyncSeriesHook: new AsyncSeriesHook(['params1', 'params2'])
        }
    }
    // 添加消息消费者
    init() {
        // hooks.asyncSeriesHook 为 AsyncSeriesHook类的实例, 使用 tapAsync / tapPromise方法添加消息的消费者
        this.hooks.asyncSeriesHook.tapAsync('pluginA', (params1, params2, cb) => {
            setTimeout(()=>{
                console.log(new Date(), 'pluginA', params1, params2)
                cb()
            }, 1000)
        })
        this.hooks.asyncSeriesHook.tapAsync('pluginB', (params1, params2, cb) => {
            setTimeout(()=>{
                console.log(new Date(), 'pluginB', params1, params2)
                cb()
            }, 2000)
        })
        this.hooks.asyncSeriesHook.tapPromise('pluginC', (params1,params2) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log(new Date(), 'pluginC', params1, params2)
                  resolve();
                }, 3000)
              })
        })
        return this
    }
    // 调用
    start() {
        // 触发asyncSeriesHook,并传入参数与回调
        this.hooks.asyncSeriesHook.callAsync({test: 'hello world'}, 'asyncSeriesHook', () => {
            console.log('all is done')
        });
    }
}

const test = new TapableTest()

test.init()
test.start()

控制台命令行执行:

node test.js

输出结果:

2022-02-24T15:53:59.401Z pluginA { test: 'hello world' } asyncSeriesHook
2022-02-24T15:54:01.410Z pluginB { test: 'hello world' } asyncSeriesHook
2022-02-24T15:54:04.411Z pluginC { test: 'hello world' } asyncSeriesHook
all is done

异步消费者注册有tapAsync和tapPromise两种方式:

  • tapAsync 形参最后将会是告知调用这消费完成的回调函数,需要在自定义逻辑执行完成后执行该回调函数。
  • tapPromise 返回一个Promise实例,需要在Promise实例构造函数入参回调中执行resolve告知调用这当前消费已完成。

异步消费者使用callAsync调用,可以传一个结束回调函数,在所有异步消费者都被执行后触发执行。控制台最后打印出all is done

由于是串行执行消费者,所以控制台日志输出的时间间隔分别是1000ms2000ms3000ms

AsyncParallelHook

异步并行任务钩子,并行执行注册回调。和同步钩子不一样,需要使用tapAsync或tapPromise注册消费者,callAsync触发消费者。

控制台命令行执行:

const { AsyncParallelHook } = require('tapable');

class TapableTest {
    constructor() {
        this.hooks = {
            // 实例化同步钩子,赋值给 hooks.asyncParallelHook, 接收两个参数。
            asyncParallelHook: new AsyncParallelHook(['params1', 'params2'])
        }
    }
    // 添加消息消费者
    init() {
        // hooks.asyncParallelHook 为 AsyncParallelHook类的实例, 使用 tapAsync / tapPromise方法添加消息的消费者
        this.hooks.asyncParallelHook.tapAsync('pluginA', (params1, params2, cb) => {
            setTimeout(()=>{
                console.log(new Date(), 'pluginA', params1, params2)
                cb()
            }, 1000)
        })
        this.hooks.asyncParallelHook.tapAsync('pluginB', (params1, params2, cb) => {
            setTimeout(()=>{
                console.log(new Date(), 'pluginB', params1, params2)
                cb()
            }, 2000)
        })
        this.hooks.asyncParallelHook.tapPromise('pluginC', (params1,params2) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log(new Date(), 'pluginC', params1, params2)
                  resolve();
                }, 3000)
              })
        })
        return this
    }
    // 调用
    start() {
        // 触发asyncParallelHook,并传入参数与回调
        this.hooks.asyncParallelHook.callAsync({test: 'hello world'}, 'asyncParallelHook', () => {
            console.log('all is done')
        });
    }
}

const test = new TapableTest()

test.init()
test.start()

输出结果:

2022-02-24T15:51:11.116Z pluginA { test: 'hello world' } asyncParallelHook
2022-02-24T15:51:12.116Z pluginB { test: 'hello world' } asyncParallelHook
2022-02-24T15:51:13.116Z pluginC { test: 'hello world' } asyncParallelHook
all is done

AsyncParallelHook的API与AsyncSeriesHook的API雷同,差别在于并行与串行:

由于是并行执行消费者,所以控制台日志输出的时间间隔分别是1000ms1000ms1000ms

总结

webpack plugin的主要Api都是继承tapable库中的类实现的。通过了解SyncHookSyncBailHookAsyncParallelHookAsyncSeriesHook类对webpack plugin下的hook api,串行、并行、同步、异步操作有一个大概的了解。以便更好的阅读接下来的内容。

了解tapable的确是webpack plugin入门中的入门,谢谢大家阅读。