Skip to content

Demo 1 · 第一个插件与可逆副作用

目标:亲手写出插件的三种形态,观察服务注册、依赖注入与"注册即清理"。对应原理篇 02。代码:demos/01-first-plugin/main.ts

运行

sh
cd demos
npm run demo:1

预期输出(节选):

text
== 创建根上下文,依次挂载插件 ==
[clock] 启动,前缀 = tick
[listener] 已监听 greet 事件
[consumer] 通过 ctx.greeter 拿到服务:你好,Cordis!
[listener] 收到 greet 事件:hello, world!
== 3 秒后卸载 GreeterService,观察级联清理 ==
[clock] tick 2026-08-13T14:18:34.052Z
…(每 400ms 一条)…
== 卸载 clockPlugin,观察 effect 逆序清理 ==
[clock] 定时器已清理(tick)
== 卸载根上下文 ==

代码拆解

形态一:函数插件

ts
function clockPlugin(ctx: Context, config: { prefix: string }) {
  console.log(`[clock] 启动,前缀 = ${config.prefix}`)
  // 非托管资源(setInterval)用 ctx.effect 包起来:
  ctx.effect(() => {
    const timer = setInterval(() => { /* tick */ }, 400)
    return () => {
      clearInterval(timer)                    // ← 插件卸载时自动执行
      console.log(`[clock] 定时器已清理(${config.prefix})`)
    }
  })
}
clockPlugin.inject = [] as string[]           // 元数据:不依赖任何服务

注意 ctx.effect 的签名:立即执行 body,body 返回一个 disposer。Cordis 保证 disposer 在插件卸载时被调用。输出里的 "定时器已清理" 不是我们手动写的调用点,而是框架在卸载时替我们执行的。

形态二与三:对象插件与服务

ts
// 对象形态
const listenerPlugin = {
  name: 'listener-plugin',
  apply(ctx: Context) {
    ctx.on('greet', (who) => console.log(`[listener] 收到 greet:${who}`))
  },
}

// 类形态:同时也是服务提供者
declare module '@deepseek-ai/cordis' {
  interface Context { greeter: GreeterService }   // 声明合并:ctx.greeter 的类型
}
class GreeterService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')                        // 注册为服务名 'greeter'
  }
  greet(name: string) { return `你好,${name}!` }
}

super(ctx, 'greeter') 就是服务的注册动作——它内部调用 ctx.provide,而 provide 本身是 effect:服务提供者卸载,服务自动消失

依赖注入

ts
function consumerPlugin(ctx: Context) {
  console.log(`[consumer] 通过 ctx.greeter 拿到服务:${ctx.greeter.greet('Cordis')}`)
}
consumerPlugin.inject = ['greeter'] as string[]

consumerPlugin 被挂载时,greeter 服务可能还不存在。inject 声明让它的 fiber 停在 PENDING,等服务就绪才执行 apply。加载顺序由依赖决定,不由挂载顺序决定——把挂载顺序倒过来(先 consumer 后 greeter),输出不变。

组装与观察

ts
const root = new Context()
root.plugin(clockPlugin, { prefix: 'tick' })
root.plugin(listenerPlugin)
const greeterFiber = root.plugin(GreeterService)
root.plugin(consumerPlugin)
await greeterFiber                       // fiber 可 await:等待加载完成
root.emit('greet', 'world')              // 派发事件

亲手做实验

实验 1:倒转挂载顺序

consumerPlugin 挂到 GreeterService 之前,输出会变吗?为什么?

实验 2:重复加载服务

root.plugin(GreeterService) 复制一份再执行。你会得到什么错误?这解释了 dsh 里"一个服务只有一个提供者"的语义。

实验 3:看清理顺序

在 clockPlugin 里再加一个 ctx.effect,让两个 effect 分别打印清理顺序。观察到的是注册顺序还是逆序?

常见错误

现象原因
service "greeter" has been registered at <GreeterService>同一个服务被加载两次(实验 2 会看到)
INACTIVE_EFFECT在已卸载的 fiber 上注册 effect/监听器
插件"没反应"可能 inject 的服务名写错,fiber 停在 PENDING 不报错(见 FAQ

下一步

你已经在 3 分钟内写完了"服务注册 + 依赖注入 + 可逆清理"的最小闭环。Demo 2 把事件总线拆开细看。

基于 DeepSeek Harness(开发者预览版 0.1.0-rc.6)与 Cordis 撰写