Skip to content

Cordis 入门 05:用插件组合组织功能

前几篇文章里,我们每次都在应用入口安装插件。真实项目里,一个功能常常由多个小插件组成:一个插件提供服务,一个插件监听事件,一个插件启动定时器,一个插件暴露工具。

Cordis 允许插件内部继续安装子插件。这个能力可以把多个小插件组合成一个功能包。

这篇要练什么

本文会实现一个父插件 messageSuite,它内部安装三个子插件:

  • loggerProvider:提供日志服务。
  • messageListener:监听消息事件。
  • messageTicker:定时发送消息事件。

你会掌握:

  • 在插件里调用 ctx.plugin()
  • 理解父插件和子插件的生命周期关系。
  • 用组合方式组织功能,而不是把所有代码塞进一个插件。

最小父子插件

先写一个最小父子插件例子。

js
import { Context } from 'cordis'

const ctx = new Context()

function childPlugin(ctx) {
  ctx.effect(() => {
    console.log('child loaded')
    return () => console.log('child disposed')
  })
}

function parentPlugin(ctx) {
  ctx.plugin(childPlugin)
}

const parentFiber = ctx.plugin(parentPlugin)
await parentFiber

await parentFiber.dispose()

输出:

text
child loaded
child disposed

父插件安装子插件后,子插件也进入 Cordis 的生命周期管理。父插件卸载时,子插件会被清理。

拆一个消息功能包

现在做一个更像真实项目的组合。

js
import { Context } from 'cordis'

const ctx = new Context()

function loggerProvider(ctx) {
  ctx.provide('appLogger', {
    info(message) {
      console.log(`[info] ${message}`)
    },
  })
}

const messageListener = Object.assign((ctx) => {
  ctx.on('demo/message', (text) => {
    ctx.appLogger.info(`message: ${text}`)
  })
}, {
  inject: ['appLogger'],
})

function messageTicker(ctx) {
  ctx.effect(() => {
    let count = 1

    const timer = setInterval(() => {
      ctx.emit('demo/message', `tick ${count++}`)
    }, 1000)

    return () => clearInterval(timer)
  }, 'message ticker')
}

function messageSuite(ctx) {
  ctx.plugin(loggerProvider)
  ctx.plugin(messageListener)
  ctx.plugin(messageTicker)
}

const suiteFiber = ctx.plugin(messageSuite)
await suiteFiber

setTimeout(async () => {
  await suiteFiber.dispose()
}, 3100)

运行后会看到类似输出:

text
[info] message: tick 1
[info] message: tick 2
[info] message: tick 3

这个例子里,入口只安装了 messageSuitemessageSuite 再安装自己的子插件。

为什么要拆成多个插件

你当然可以把所有逻辑都写进一个大插件。

js
function bigPlugin(ctx) {
  ctx.provide('appLogger', ...)
  ctx.on('demo/message', ...)
  ctx.effect(...)
}

入门 demo 这样写没问题,但项目变大后会有几个问题:

  • 日志服务无法单独复用。
  • 事件监听逻辑无法单独测试。
  • 定时器和服务注册混在一起,清理边界不清楚。
  • 某个子能力出问题时,不容易定位。

拆成多个插件后,每个插件只负责一件事。父插件只负责编排。

插件职责
loggerProvider提供日志服务
messageListener消费日志服务,监听消息事件
messageTicker定时发送消息事件
messageSuite组合前三个插件

这就是插件组合的价值:小插件负责能力,大插件负责装配。

小练习:给组合插件加配置

messageSuite 支持配置间隔时间。

js
import { Context } from 'cordis'

const ctx = new Context()

function loggerProvider(ctx) {
  ctx.provide('appLogger', {
    info(message) {
      console.log(`[info] ${message}`)
    },
  })
}

const messageListener = Object.assign((ctx) => {
  ctx.on('demo/message', (text) => {
    ctx.appLogger.info(`message: ${text}`)
  })
}, {
  inject: ['appLogger'],
})

function createMessageTicker(interval) {
  return function messageTicker(ctx) {
    ctx.effect(() => {
      let count = 1

      const timer = setInterval(() => {
        ctx.emit('demo/message', `tick ${count++}`)
      }, interval)

      return () => clearInterval(timer)
    }, 'message ticker')
  }
}

function messageSuite(ctx, config) {
  ctx.plugin(loggerProvider)
  ctx.plugin(messageListener)
  ctx.plugin(createMessageTicker(config.interval))
}

const suiteFiber = ctx.plugin(messageSuite, {
  interval: 500,
})

await suiteFiber

setTimeout(async () => {
  await suiteFiber.dispose()
}, 1600)

这个写法用工厂函数创建带配置的子插件。入门阶段这样最直观。更复杂的项目可以把配置拆给各个子插件。

入门检查点

插件组合要记住这句话:

text
父插件负责装配,子插件负责单一能力

这样写出来的插件更容易拆分、测试和复用。下一篇我们把前面所有内容合起来,做一个迷你插件运行时:有日志服务、消息存储、定时事件和可卸载的功能包。


系列导航