技术·MiniCordis:从零实现一个插件化 Runtime · 第 5 篇·2026-08-27·7 分钟

第四篇:从一次性依赖检查到响应式依赖生命周期

这一篇要解决的问题是:当一个 Plugin 依赖的能力可能在运行过程中出现、消失和恢复时,Runtime 应该如何持续维护它的激活状态。为了把这个问题讲清楚,我们会先从一次性的依赖检查开始,再看为什么这还远远不够,最后让依赖关系本身变成 Runtime 持续追踪的对象。

1. 这一篇要解决的问题

前面几篇解决的主要还是“能力如何安装”“能力如何撤销”“模块如何解耦通信”。但真实的 Runtime 往往还要面对另一类问题:某个能力并不是一直可用的。比如 App 想依赖 greeter,但 greeter 可能还没安装,也可能运行一段时间后被卸载,然后将来再装回来。

如果只是把 ctx.get('greeter') 当作普通查表,这个问题其实没有被真正解决。因为调用方必须自己不断判断:“它现在在不在?如果不在,我是不是该停掉?如果又回来了,我是不是该重新启动?”

所以这一篇真正要解决的问题是:

当依赖的可用状态本身会变化时,Runtime 该如何把这种变化纳入生命周期管理?

这对应 mini-cordis 里的 v0.8 和 v0.9。

这一篇的主线可以先压缩成下面这条路径:

text
依赖此刻是否存在
   ↓
require 只能检查一次
   ↓
inject 长期记住依赖关系
   ↓
依赖变化时自动激活 / 撤销
依赖生命周期总览图
依赖生命周期总览图

2. 上一阶段卡在哪里

最直接的第一步,当然是做一次性检查,也就是:

ts
ctx.require(['greeter'], (ctx) => {
  // 依赖当前满足时执行
})

这确实比调用方自己手写 if (!ctx.has('greeter')) 更整齐,但它本质上仍然只是一个“此刻快照”。检查完、执行完,这个调用就结束了。之后如果 greeter 被卸载,Runtime 并不会自动回来收拾之前激活的那段逻辑;如果未来又重新安装了 greeter,Runtime 也不会主动重新激活它。

也就是说,require() 只能回答:

text
依赖现在满不满足?

却回答不了:

text
依赖之后变了怎么办?

而一个真正的插件化 Runtime 恰恰必须处理后者。

3. 这一篇引入什么机制

为了解决这个问题,MiniCordis 最后引入的是:

ts
ctx.inject(['greeter'], (ctx) => {
  // 依赖满足时激活
  // 依赖消失时撤销
  // 依赖再次满足时重新激活
})

表面上看,inject() 像是 require() 的增强版;但它真正改变的不是语法,而是模型。require() 只是一次调用,而 inject() 会在 Runtime 内部留下一条长期存在的依赖记录。每次 provide() 或 remove() 发生时,系统都会重新检查这些记录,决定对应 callback 应该激活、停用还是重新激活。

更关键的是,inject() 并没有重新发明一套生命周期逻辑。它的“激活”本质上仍然是:

text
打开一个新的 scope
运行 callback
记住这个 scope 的 dispose

也就是说,依赖满足后的激活,和前面讲过的 Plugin 安装,在 Runtime 内部其实是同一种事。区别只是:Plugin 通常安装一次就保持运行,而 inject() 的 callback 会随着依赖变化反复激活和撤销。

4. 代码里真正加了什么

从代码角度看,这一阶段的关键是让 Context 维护一组长期存在的依赖记录:

ts
interface Injection {
  names: string[]
  callback: (ctx: Context) => void
  dispose: (() => void) | null
}

然后在 provide() 和 remove() 之后统一重新检查这些记录:

ts
provide(name: string, service: unknown): void {
  this.services.set(name, service)
  this.reevaluateAll()
}

remove(name: string): void {
  this.services.delete(name)
  this.reevaluateAll()
}

inject() 自己则负责把依赖关系注册进 Context,并根据当前状态立刻做第一次判断:

ts
inject(names: string[], callback: (ctx: Context) => void): () => void {
  const injection = { names, callback, dispose: null }
  this.injections.add(injection)
  this.reevaluate(injection)
  // ...
}

这套机制真正重要的地方,不在于它有多复杂,而在于它把“依赖关系”本身也纳入了 Runtime 的第一类对象。系统不再只知道“有哪些 service”,也开始知道“谁依赖谁,以及依赖变化时要怎么反应”。

从这里开始,MiniCordis 才真正从一个带 Map、带 cleanup 的容器,变成了一个 Dynamic Runtime。

5. 这一篇的结论

这一篇最重要的结论是:一旦依赖会在运行过程中变化,问题就不再是“如何检查依赖是否存在”,而是“Runtime 是否会持续记住这条依赖关系,并在状态变化时自动响应”。require() 只解决了静态检查,inject() 则第一次把依赖生命周期真正纳入了 Runtime。

下一篇会在这个基础上继续推进。只要系统开始长期运行,就不能只关心依赖是否会变化,还必须处理两个更现实的问题:某个 Plugin 失败时不能拖垮整个系统,以及一个 Context 最终往往不止一个节点,而是一棵树。

《MiniCordis:从零实现一个插件化 Runtime》合集 · 第 5 / 7 篇