如何实现一个Plugin机制
假如我们现在有一个 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
已经是一个复杂度可控的类了。
支持异步函数调用
现在新的需求又来了,prapareNode
和prepareYarn
是一个异步的执行函数。我们需要在异步函数prapareNode
和prepareYarn
执行完毕之后才能够调用 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
通过包装EventEmitter
的on
和emit
事件,并且内置了一个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
接受两种类型分别为parallel
或series
,实现了支持异步函数的并行或串行。甚至实现了可以将上一个函数的执行结果作为下一个函数的参数。
至此一个支持同步、异步、并行、串行的插件系统已经完成啦 🎉🎉🎉
终极实现 Tapable
Tapable
是Webpack
团队开发的基于事件驱动的插件模块,Webpack
的Plugins
机制就是基于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行代码已经实现了。
总结
对于一些较复杂的项目,插件化的开发方式,可以让我们的项目更加灵活,同时极高的增加项目的可维护性。我们常用的Webpack
、Rollup
等都存在Plugins
机制的设计思想。通过定义好生命周期事件,之后暴露给外部介入,从而实现了不同的功能通过不同的插件来实现的行为。
通过一步步的实现一个自己的Plugin
机制,到最终改为通过Tapable
实现,通过这个过程相信你也会对Webpack
的Plugin
有更好的理解。
转载自:https://juejin.cn/post/7122756678574932005