插件系统设计

文章目录

  • 1、从0开始
  • 2、环境准备,依赖注入
  • 3、控制任务时序
  • 4、终极大招

1、从0开始

  • 假如我们现在有一个 Runner类如下,外界可以通过调用exec方法来执行内部逻辑进行build的操作
class Runner {exec() {build()}
}
function build() {console.log('build')
}
  • 但是,我们想要在进行buildSomething之前来进行一些前期的准备工作,如准备node环境或者yarn。更改代码如下:
class Runner {exec() {prepareNode()prepareYarn()build()}
}
function prepareNode() {console.log('prepareNode')
}
function prepareYarn() {console.log('prepareYarn')
}function build() {console.log('build')
}
  • 下一步,容易想到可以抽离出来一个prepare函数,将所有的准备工作都丢在prepare函数中,来保证Runner类的纯粹
class Runner {exec() {prepare()buildSomething()}
}
function prepare() {prepareNode()prepareYarn()
}
...
  • 下一步,在build之前,不仅想执行prepare,还想执行一些其他的预置操作,比如beforeBuild等,代码就会变成这样
    • 可以预想到,配置越多,增加的类似prepare方法就越多
class Runner {exec() {prepare()beforeBuild()...build()}
}
function prepare() {prepareNode()prepareYarn()
}function beforeBuild(){...
}
...

2、环境准备,依赖注入

  • 下一步,通过配置参数的形式传入,再使用EventEmitter来注册任务
const EventEmitter = require("events").EventEmitter;class Runner {eventTriger;constructor(options) {this.eventTriger = new EventEmitter();if (Array.isArray(options.prepare)) {for (const listener of options.prepare) {this.eventTriger.on("prepare", listener);}} else {this.eventTriger.on("prepare", options.prepare);}}exec() {this.eventTriger.emit("prepare");build();}
}function build() {console.log("build something");
}const runner = new Runner({prepare: [prepareNode, prepareYarn],
});
runner.exec();function prepare() {prepareNode();prepareYarn();
}

3、控制任务时序

  • 支持异步函数调用
    • 需要在异步函数 prepare执行完毕之后才能够调用 build 的操作
const EventEmitter = require("events").EventEmitter;class Runner {eventTriger;//处理event上下文eventHandlerContext = new Map()constructor(options) {this.eventTriger = new EventEmitter();if (Array.isArray(options.prepare)) {for (const listener of options.prepare) {this._on('prepare', listener)}} else {this._on('prepare', options.prepare)}}_on(eventName, listener) {//注册的是一个包装的方法const wrapperListener = async (...args) => {const ctx = this.eventHandlerContext.get(eventName);const ret = await listener(...args)this.eventHandlerContext.set(eventName, { ...ctx, count: ctx.count++ })// 类似Promise.all的实现,当已经执行完毕的方法和注册的方法一致时,resolve掉exec的Promiseif (ctx.count === this.eventTriger.listenerCount(eventName)) {ctx.resolve(true)}return ret}this.eventTriger.on(eventName, wrapperListener);}async _emit(eventName, ...args) {const promise = new Promise((resolve, reject) => {//存储当前event对应的状态this.eventHandlerContext.set(eventName, { reject, resolve, count: 0 })})this.eventTriger.emit(eventName)await promise}async exec() {await this._emit("prepare");build();}
}function build() {console.log("build something");
}const runner = new Runner({prepare: [prepareNode, prepareYarn],
});
runner.exec();function sleep(wait) {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')
}function build() {console.log('build')
}//代码执行结果如下
//prepareYarn
//prepareNode
//build something
  • 支持异步并行和异步串行
    • 在上一步中,实现了支持异步函数调用,但prepareNode的调用会晚于prepareYarn
    • 继续改造实现异步串行调用,这里需要注意异步await 不能使用forEach这样的通过回掉的方式实现,普通的for…of…等即可
    • 在异步串行中,支持传递上一个函数调用的结果

async function timer3() {await new Promise((resolve) => {setTimeout(() => {console.log('3')resolve()}, 3000)})
}async function timer2() {await new Promise((resolve) => {setTimeout(() => {console.log('2')resolve()}, 2000)})
}async function timer1() {await new Promise((resolve) => {setTimeout(() => {console.log('1')resolve()}, 1000)})
}let arr = [timer3, timer2, timer1];arr.forEach(async (fn) => {await fn()
})//输出结果:1、2、3;
//forEach对回掉的处理,只是简单的调用,不能实现阻塞
//while (index < arr.length) {//callback(item, index)   //也就是我们传入的回调函数
//}
  • 所以实现异步串行需要类似这样的操作

for (const listener of listeners:Function[]) {await listener
}
  • 最终实现

const EventEmitter = require("events").EventEmitter;class Runner {eventTriger;//处理event上下文eventHandlerContext = new Map()//收集事件名eventNames = []//收集对应的事件类型eventTypeStore = new Map()//针对异步串行的状态管理seriesListenersStore = new Map()constructor(options) {this.eventTriger = new EventEmitter();this.eventNames = Object.keys(options);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)}}}_on(eventName, listener) {const eventType = this.eventTypeStore.get(eventName)switch (eventType) {case 'parallel': {const wrapperListener = async (...args) => {const ctx = this.eventHandlerContext.get(eventName)await listener(...args)this.eventHandlerContext.set(eventName, { ...ctx, count: ctx.count++ })// 如果listener 全部执行完毕 代表 on 事件执行完毕if (ctx.count === this.eventTriger.listenerCount(eventName)) {ctx.resolve(true)}}this.eventTriger.on(eventName, wrapperListener)break;}//异步串行case 'series': {//异步串行需要收集虽有方法,然后只注册一次即可,所以这里每次注销上次注册的回掉this.eventTriger.removeAllListeners(eventName)//从seriesListenersStore中获取已存储的方法const listeners = (this.seriesListenersStore.get(eventName) || [])listeners.push(listener)//将当前收集到的方法更新存储this.seriesListenersStore.set(eventName, listeners)let wrapperListener = async (...args) => {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.eventTriger.on(eventName, wrapperListener)break}default:throw new Error(`unknown event type ${this.eventTypeStore.get(eventName)}`)}}async _emit(eventName, ...args) {const promise = new Promise((resolve, reject) => {this.eventHandlerContext.set(eventName, { reject, resolve, count: 0 })})this.eventTriger.emit(eventName, ...args)await promise}async exec(...args) {//for ... of ...异步串行调用for (const eventName of this.eventNames) {console.log(`-----------${eventName}生命周期执行-----------`)await this._emit(eventName, ...args)}build();}
}function build() {console.log("build something");
}const runner = new Runner({prepare: {listeners: [prepareNode, prepareYarn],type: 'parallel'},start: {listeners: [prepareNode, prepareYarn],type: 'series'},end: {listeners: [pipe1, pipe2],type: 'series'}
})
runner.exec(1, 2);function sleep(wait) {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')
}
function pipe1(a, b) {console.log(a, b, 'pipe1执行')return a + b
}
function pipe2(a) {console.log(a, 'pipe2执行')return a * 2
}function build() {console.log('build')
}
  • 至此一个支持同步、异步、并行、串行的插件系统已经完成啦

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

4、终极大招

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

const {AsyncParallelHook,AsyncSeriesHook,AsyncSeriesWaterfallHook} = require("tapable");class TapableRunner {hooks: { prepare: AsyncParallelHook, start: AsyncSeriesHook, end: AsyncSeriesWaterfallHook}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执行
  • 总结:对于一些较复杂的项目,插件化的开发方式,可以让我们的项目更加灵活,同时极高的增加项目的可维护性。我们常用的 WebpackRollup 等都存在 Plugins 机制的设计思想。通过定义好生命周期事件,之后暴露给外部介入,从而实现了不同的功能通过不同的插件来实现的行为


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部