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

第一篇:从空的 Context 到最小 Service Registry

这一篇要解决的问题是:在谈 Plugin、Event 和依赖之前,一个最小 Runtime 至少应该先拥有什么。为了把这个问题讲清楚,我们会先从一个完全没有行为的 Context 开始,再逐步给它加入“注册能力”“读取能力”“移除能力”这几件最基本的事情。

1. 这一篇要解决的问题

如果一上来就把 Plugin、Effect、Event 和依赖系统全塞进 Context,读者很容易分不清两件事:一是 Context 这个容器本身是什么,二是后续那些机制到底是怎样挂到这个容器上的。MiniCordis 最早的几个版本之所以故意保持克制,就是为了先把这条边界单独立住。

换句话说,这一篇真正的问题不是“怎样做一个厉害的 Runtime”,而是更基础的:

在一切更复杂的机制出现之前,Runtime 至少要有一个明确的宿主对象,以及一张能表达“当前有哪些能力”的最小能力表。

这对应 mini-cordis 里的 v0.1、v0.2 和 v0.3。

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

text
空的 Context
   ↓
能注册能力
   ↓
能读取能力
   ↓
能判断和移除能力
Context 与能力表总览图
Context 与能力表总览图

2. 为什么要从这里开始

如果没有 Context,后面所有能力都没有落脚点;但如果有了 Context,它却只是一个空对象,那么 Runtime 仍然无法表达“这里现在有一个 greeter 能力”。调用方只能直接 import 具体实现,或者自己 new 一个对象,然后把它握在手里使用。

这会立即带来两个问题。首先,调用方会和具体实现绑定在一起,没法只依赖“能力名字”;其次,系统完全无法表达“这个能力现在还在不在”,因为根本没有一张统一的表来记录这件事。

所以在最开始的阶段,MiniCordis 并不急着处理安装、撤销或隔离,而是先把下面三个更小的问题说明白:

text
Context 作为容器到底是什么?
能力应该存在哪里?
能力如果会消失,Runtime 应该怎么表达?

3. 这一篇引入什么机制

第一步是一个几乎空到不能再空的定义:

ts
export class Context {}

这看起来几乎什么都没做,但它非常重要,因为它先给 Runtime 建立了一个明确的宿主对象。后续所有机制都会逐步加到这个类上,而不是散落成一组彼此无关的函数。换句话说,哪怕这一刻 Context 还是空的,它也已经先把“系统的中心在哪里”这件事定下来了。

第二步是最小的 Service Registry:

ts
ctx.provide('greeter', greeter)
ctx.get('greeter')

内部只需要一张很简单的表:

ts
private services = new Map<string, unknown>()

有了这一步以后,Runtime 才第一次能够明确表达:

text
当前 Context 里有一个名为 greeter 的能力

第三步是补上“能力不是永远存在”的事实,也就是加入:

ts
ctx.has('greeter')
ctx.remove('greeter')

于是 Context 就不再只是一张会增长、不会缩小的表,而是开始具备最基础的生命周期语义:能力可以出现,也可以消失。

4. 代码里真正加了什么

从代码角度看,这一阶段的变化其实很有限。Context 先从一个空类开始,然后逐渐长成下面这样:

ts
export class Context {
  private services = new Map<string, unknown>()

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

  get(name: string): unknown {
    return this.services.get(name)
  }

  has(name: string): boolean {
    return this.services.has(name)
  }

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

这里有两个设计选择值得特别注意。第一,get() 返回的是 unknown,并没有在这一步就把类型系统做得很复杂。这是刻意的,因为 MiniCordis 在这个阶段更想讲清楚 Runtime 模型,而不是先把类型推导做满。第二,remove() 和 has() 的加入意味着系统已经开始承认:能力是会变化的,而不是一旦注册就永久存在。

从教学顺序上看,这里还有一个很重要的节制:我们还没有引入 Plugin。也就是说,此时注册能力的动作仍然发生在调用现场:

ts
ctx.provide('greeter', ...)

这也正是下一篇的切入点。因为很快我们就会发现:当调用方自己直接操作 provide 时,“能力本身是什么”和“能力怎样装进 Context”这两件事其实被混在了一起。

5. 这一篇的结论

这一篇最重要的结论是:一个最小 Runtime 在真正变复杂之前,至少要先有一个明确的容器对象,以及一张能够表达“能力存在、读取和消失”的最小能力表。Context 和 Service Registry 看起来都很朴素,但后面所有机制都会建立在这一层基础之上。

下一篇会在这个基础上继续推进。只要再问一句“谁来安装这些能力、谁来撤销这些能力”,系统就会立刻从简单的能力表走向 Plugin、Effect 和 Scope。

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