likes
comments
collection
share

如何实现一个Plugin机制

作者站长头像
站长
· 阅读数 15

假如我们现在有一个 Runner类如下,外界可以通过调用exec方法来执行内部逻辑进行builSomething的操作。如下:

class Runner {
    exec() {
        buildSomething()
    }
}
function buildSomething() {
    console.log('buildSomething')
}

这很容易理解。但是,我们想要在进行buildSomething之前来进行一些前期的准备工作,如准备node环境或者yarn。更改代码如下:

class Runner {
    exec() {
        prepareNode()
        prepareYarn()
        buildSomething()
    }
}
function prepareNode() {
    console.log('prepareNode')
}
function prepareYarn() {
    console.log('prepareYarn')
}

function buildSomething() {
    console.log('buildSomething')
}

我们很容易想到可以抽离出来一个prepare函数,将所有的准备工作都丢在prepare函数中,来保证Runner类的纯粹。如下:

class Runner {
    exec() {
        prepare()
        buildSomething()
    }
}
function prepare() {
    prepareNode()
    prepareYarn()
}

但是此时我们有了更复杂的需求,假如在exec执行之前可能是执行prepareNode操作,也可能是prepareYarn还有可能是两者都要进行准备。继续更改代码如下:

interface Options {
    shouldPrepareNode: boolean,
    shouldPrepareYarn: boolean
}
class Runner {
    protected shouldPrepareNode: boolean
    protected shouldPrepareYarn: boolean
    constructor(options: Options) {
        this.shouldPrepareNode = options.shouldPrepareNode
        this.shouldPrepareYarn = options.shouldPrepareYarn
    }
    exec() {
        prepare.call(this)
        buildSomething()
    }
}
function prepare(this: Runner) {
    this.shouldPrepareNode && prepareNode()
    this.shouldPrepareNode && prepareYarn()
}

此时已经实现了我们的需求,但是需求是无止尽的! 现在我要在exec之前执行install操作。或者我想要在exec之前执行任何其他不确定的操作。那么我们应该如何处理Runner类呢,无限制的增加shouldPrepareInstall或者无尽的shouldPreparexxx参数?一定不会有人选择这么操作!

简易的插件实现

Nodejs 为我们提供了一个events模块,现在我们通过events模块来优化最初的代码:

class Runner {
    private ee: EventEmitter
    constructor(options: Options) {
        this.ee = new EventEmitter()
        options.shouldPrepareNode && this.ee.on('prepare', () => {
            console.log('prepareNode')
        })
        options.shouldPrepareYarn && this.ee.on('prepare', () => {
            console.log('prepareYarn')
        })
    }
    exec() {
        this.ee.emit('prepare')
        buildSomething()
    }
}
function buildSomething() {
    console.log('buildSomething')
}
const runner = new Runner({ shouldPrepareNode: true, shouldPrepareYarn: false })
runner.exec() 
//  prepareNode
//  buildSomething

上面我们说过,我们需要满足在exec执行之前能够执行任何的操作,但是我们不可能会无限制的增加shouldPreparxxx来进行实现。那么所有的preparexxx操作都应该以参数的形式传入。继续优化我们的代码如下:

type Listener = (...args: any[]) => void
interface Options {
    prepareListeners: Listener[] | Listener,
}
class Runner {
    private ee: EventEmitter
    constructor(options: Options) {
        this.ee = new EventEmitter()
        if (Array.isArray(options.prepareListeners)) {
            for (const listener of options.prepareListeners) {
                this.ee.on('prepare', listener)
            }
        } else {
            this.ee.on('prepare', options.prepareListeners)
        }
    }
    exec() {
        this.ee.emit('prepare')
        buildSomething()
    }
}

const runner = new Runner({
    prepareListeners: [
        prepareNode,
        prepareYarn
    ]
})
runner.exec()

此时已经实现了一个简易版本的插件系统,并且Runner已经是一个复杂度可控的类了。

支持异步函数调用

现在新的需求又来了,prapareNodeprepareYarn是一个异步的执行函数。我们需要在异步函数prapareNodeprepareYarn执行完毕之后才能够调用 buildSomething的操作。如下:

function sleep(wait: number) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(wait)
        }, wait);
    })
}
async function prepareNode() {
    await sleep(500)
    console.log('prepareNode')
}
async function prepareYarn() {
    await sleep(300)
    console.log('prepareYarn')
}

此时我们的执行结果为:

buildSomething
prepareYarn
prepareNode

很明显,buildSomething 的执行早于了preparexxx。这是因为我们的emit过程并不会被await,让我们继续改造代码让 emit 函数可以被await。如下:

type Listener = (...args: any[]) => void
interface Options {
    prepareListeners: Listener[] | Listener,
}
type LifeCycle = 'prepare' | 'start' | 'end' | 'xxx'
interface Context {
    count: number,
    resolve: (value: unknown) => void
    reject: (value: unknown) => void
}
class Runner {
    private eventHandlerContext = new Map<LifeCycle, Context>()
    private ee: EventEmitter
    constructor(options: Options) {
        this.ee = new EventEmitter()
        if (Array.isArray(options.prepareListeners)) {
            for (const listener of options.prepareListeners) {
                this._on('prepare', listener)
            }
        } else {
            this._on('prepare', options.prepareListeners)
        }
    }
    async exec() {
        await this._emit('prepare')
        buildSomething()
    }
    private _on(eventName: LifeCycle, listener: Listener) {
        const wrapperListener = async (...args: any[]) => {
            const ctx = this.eventHandlerContext.get(eventName)!
            const ret = await listener(...args)
            this.eventHandlerContext.set(eventName, { ...ctx, count: ctx.count++ })
            // 如果listener 全部执行完毕 代表 on 事件执行完毕
            if (ctx.count === this.ee.listenerCount(eventName)) {
                ctx.resolve(true)
            }
            return ret
        }
        this.ee.on(eventName, wrapperListener)
    }
    private async _emit(eventName: LifeCycle, ...args: any[]) {
        const promise = new Promise((resolve, reject) => {
            this.eventHandlerContext.set(eventName, { reject, resolve, count: 0 })
        })
        this.ee.emit(eventName)
        await promise
    }
}
const runner = new Runner({
    prepareListeners: [
        prepareNode,
        prepareYarn
    ]
})
runner.exec()

此时代码的执行结果如下:

prepareYarn
prepareNode
build something

通过包装EventEmitteronemit事件,并且内置了一个eventHandlerContext来存储函数执行的状态,我们就可以支持异步的preparexxx了。

此时无论传入的preparexxx是异步操作还是同步操作,我们都可以保证buildSomething的执行是在parparexxx函数执行完成之后才会被调用。

支持异步串行或并行执行

上面代码的输出你可能已经发现了问题,由于prepareYarn函数内部异步等待了300ms执行,但是prepareNode函数内部异步等待了500ms执行,导致 prepareYarn 的输出早于prepareNode

这是因为我们上面的实现,实际上类似于Promisea.all(listeners:Function[])的操作,只是保证了buildSomething的操作在preparexxx之后执行。

如果我们想要实现 preparexxx 的顺序执行,即 prepareNode===>prepareYarn===>preparexxx===>buildSomething,就需要类似下面的操作:

for (const listener of listeners:Function[]) {
  await listener
}

继续更改代码如下

type Listener = (...args: any[]) => void
type EventName = 'prepare' | 'start' | 'end' | 'xxx'
interface Context {
    count: number,
    resolve: (value: unknown) => void
    reject: (value: unknown) => void
}
type EventType = 'parallel' | 'series'
interface Handler {
    type?: EventType,
    listeners: Listener[] | Listener
}
type Options = Partial<Record<EventName, Handler>>
class Runner {
    private eventHandlerContext = new Map<EventName, Context>()
    private eventNames: EventName[] = []
    private ee: EventEmitter
    private eventTypeStore = new Map<EventName, EventType>()
    private seriesListenersStore = new Map<EventName, Listener[]>()
    constructor(options: Options) {
        this.ee = new EventEmitter()
        this.eventNames = Object.keys(options) as EventName[]
        for (const eventName of this.eventNames) {
            const { type = 'parallel', listeners } = options[eventName]!
            this.eventTypeStore.set(eventName, type)
            const listenersArr = Array.isArray(listeners) ? listeners : [listeners]
            for (const listener of listenersArr) {
                this._on(eventName, listener)
            }
        }
    }
    async exec(...args: any[]) {
        for (const eventName of this.eventNames) {
            console.log(`-----------${eventName}生命周期执行-----------`)
            await this._emit(eventName, ...args)
        }
        buildSomething()
    }
    private _on(eventName: EventName, listener: Listener) {
        const eventType = this.eventTypeStore.get(eventName)
        switch (eventType) {
            case 'parallel': {
                const wrapperListener = async (...args: any[]) => {
                    const ctx = this.eventHandlerContext.get(eventName)!
                    const ret = await listener(...args)
                    this.eventHandlerContext.set(eventName, { ...ctx, count: ctx.count++ })
                    // 如果listener 全部执行完毕 代表 on 事件执行完毕
                    if (ctx.count === this.ee.listenerCount(eventName)) {
                        ctx.resolve(true)
                    }
                    return ret
                }
                this.ee.on(eventName, wrapperListener)
                break;
            }
            case 'series': {
                this.ee.removeAllListeners(eventName)
                const listeners = (this.seriesListenersStore.get(eventName) || [])
                listeners.push(listener)
                this.seriesListenersStore.set(eventName, listeners)
                let wrapperListener = async (...args: any[]) => {
                    const ctx = this.eventHandlerContext.get(eventName)
                    const firstFn = listeners.shift()!
                    let result = await firstFn(...args)
                    for (const fn of listeners) {
                        result = await fn(result)
                    }
                    ctx?.resolve(true)
                }
                this.ee.on(eventName, wrapperListener)
                break
            }
            default:
                throw new Error(`unknown event type ${this.eventTypeStore.get(eventName)}`)
        }
    }
    private async _emit(eventName: EventName, ...args: any[]) {
        const promise = new Promise((resolve, reject) => {
            this.eventHandlerContext.set(eventName, { reject, resolve, count: 0 })
        })
        this.ee.emit(eventName, ...args)
        await promise
    }
}
const runner = new Runner({
    prepare: {
        listeners: [prepareNode, prepareYarn],
        type: 'parallel'
    },
    start: {
        listeners: [prepareNode, prepareYarn],
        type: 'series'
    },
    end: {
        listeners: [pipe1, pipe2],
        type: 'series'
    }
})

function pipe1(a: number, b: number) {
    console.log(a, b, 'pipe1执行')
    return a + b
}
function pipe2(a: number) {
    console.log(a, 'pipe2执行')
    return a * 2
}
runner.exec(1, 2)

输出如下:

-----------prepare生命周期执行-----------
prepareYarn
prepareNode
-----------start生命周期执行-----------
prepareNode
prepareYarn
-----------end生命周期执行-----------
1 2 pipe1执行
3 pipe2执行
buildSomething

通过增加一个参数 type 接受两种类型分别为parallelseries,实现了支持异步函数的并行或串行。甚至实现了可以将上一个函数的执行结果作为下一个函数的参数。

至此一个支持同步、异步、并行、串行的插件系统已经完成啦 🎉🎉🎉

终极实现 Tapable

TapableWebpack 团队开发的基于事件驱动的插件模块,WebpackPlugins机制就是基于Tapable实现。下面我们用Tapable来实现一下上面的例子。

class TapableRunner {
  hooks: { prepare: AsyncParallelHook<void>, start: AsyncSeriesHook<void>, end: AsyncSeriesWaterfallHook<[number, number]> }
  constructor() {
    this.hooks = {
      prepare: new AsyncParallelHook(),
      start: new AsyncSeriesHook(),
      end: new AsyncSeriesWaterfallHook(['a', 'b'])
    }
  }
  async exec() {
    await this.hooks.prepare.promise()
    await this.hooks.start.promise()
    await this.hooks.end.promise(1, 2)
  }
}
const tapableRunner = new TapableRunner()
tapableRunner.hooks.prepare.tapPromise('prepareNode', prepareNode)
tapableRunner.hooks.prepare.tapPromise('prepareYarn', prepareYarn)
tapableRunner.hooks.start.tapPromise('prepareNode', prepareNode)
tapableRunner.hooks.start.tapPromise('prepareYarn', prepareYarn)
tapableRunner.hooks.end.tapPromise('pipe1', pipe1)
tapableRunner.hooks.end.tapPromise('pipe2', pipe2)
tapableRunner.exec()

输出:

prepareYarn
prepareNode
prepareNode
prepareYarn
1 2 pipe1执行
3 pipe2执行

至此,我们费劲实现的Plugin机制,通过Tapable短短20行代码已经实现了。

总结

对于一些较复杂的项目,插件化的开发方式,可以让我们的项目更加灵活,同时极高的增加项目的可维护性。我们常用的WebpackRollup等都存在Plugins机制的设计思想。通过定义好生命周期事件,之后暴露给外部介入,从而实现了不同的功能通过不同的插件来实现的行为。

通过一步步的实现一个自己的Plugin机制,到最终改为通过Tapable实现,通过这个过程相信你也会对WebpackPlugin有更好的理解。

转载自:https://juejin.cn/post/7122756678574932005
评论
请登录