这一篇要解决的问题是:当一个 Plugin 依赖的能力可能在运行过程中出现、消失和恢复时,Runtime 应该如何持续维护它的激活状态。为了把这个问题讲清楚,我们会先从一次性的依赖检查开始,再看为什么这还远远不够,最后让依赖关系本身变成 Runtime 持续追踪的对象。
1. 这一篇要解决的问题
前面几篇解决的主要还是“能力如何安装”“能力如何撤销”“模块如何解耦通信”。但真实的 Runtime 往往还要面对另一类问题:某个能力并不是一直可用的。比如 App 想依赖 greeter,但 greeter 可能还没安装,也可能运行一段时间后被卸载,然后将来再装回来。
如果只是把 ctx.get('greeter') 当作普通查表,这个问题其实没有被真正解决。因为调用方必须自己不断判断:“它现在在不在?如果不在,我是不是该停掉?如果又回来了,我是不是该重新启动?”
所以这一篇真正要解决的问题是:
当依赖的可用状态本身会变化时,Runtime 该如何把这种变化纳入生命周期管理?
这对应 mini-cordis 里的 v0.8 和 v0.9。
这一篇的主线可以先压缩成下面这条路径:
依赖此刻是否存在
↓
require 只能检查一次
↓
inject 长期记住依赖关系
↓
依赖变化时自动激活 / 撤销2. 上一阶段卡在哪里
最直接的第一步,当然是做一次性检查,也就是:
ctx.require(['greeter'], (ctx) => {
// 依赖当前满足时执行
})这确实比调用方自己手写 if (!ctx.has('greeter')) 更整齐,但它本质上仍然只是一个“此刻快照”。检查完、执行完,这个调用就结束了。之后如果 greeter 被卸载,Runtime 并不会自动回来收拾之前激活的那段逻辑;如果未来又重新安装了 greeter,Runtime 也不会主动重新激活它。
也就是说,require() 只能回答:
依赖现在满不满足?却回答不了:
依赖之后变了怎么办?而一个真正的插件化 Runtime 恰恰必须处理后者。
3. 这一篇引入什么机制
为了解决这个问题,MiniCordis 最后引入的是:
ctx.inject(['greeter'], (ctx) => {
// 依赖满足时激活
// 依赖消失时撤销
// 依赖再次满足时重新激活
})表面上看,inject() 像是 require() 的增强版;但它真正改变的不是语法,而是模型。require() 只是一次调用,而 inject() 会在 Runtime 内部留下一条长期存在的依赖记录。每次 provide() 或 remove() 发生时,系统都会重新检查这些记录,决定对应 callback 应该激活、停用还是重新激活。
更关键的是,inject() 并没有重新发明一套生命周期逻辑。它的“激活”本质上仍然是:
打开一个新的 scope
运行 callback
记住这个 scope 的 dispose也就是说,依赖满足后的激活,和前面讲过的 Plugin 安装,在 Runtime 内部其实是同一种事。区别只是:Plugin 通常安装一次就保持运行,而 inject() 的 callback 会随着依赖变化反复激活和撤销。
4. 代码里真正加了什么
从代码角度看,这一阶段的关键是让 Context 维护一组长期存在的依赖记录:
interface Injection {
names: string[]
callback: (ctx: Context) => void
dispose: (() => void) | null
}然后在 provide() 和 remove() 之后统一重新检查这些记录:
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,并根据当前状态立刻做第一次判断:
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 最终往往不止一个节点,而是一棵树。