技术·Cordis 从零开始 · 第 2·2026-08-18·7 分钟

第一篇:从一个变量开始,理解 Context、Service 和 Plugin

这一篇要解决的问题很简单:当系统里只有一个简单能力时,为什么普通变量已经够用;而当我们改用 Cordis 重写同样的需求时,ContextServicePlugin 分别是在替代什么。为了把这个问题讲清楚,我们会先写一个完全不需要框架的版本,再用 Cordis 重写同样的逻辑,最后看两者究竟从哪里开始分叉。

1. 这一篇要解决的问题

我们的需求很简单:有英文问候,有中文问候,而且程序在运行时可以切换语言。此时系统里还没有动态依赖、资源清理或模块协作问题,所以这个起点很重要,因为它能帮助我们区分“业务本身的复杂度”和“框架带来的复杂度”。

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

text
一个变量

Context 收住能力

Service 变成可访问的名字

Plugin 负责安装与替换
变量、Service 与 Plugin 关系图
变量、Service 与 Plugin 关系图

2. Vanilla:一个变量已经足够

先定义一个 Greeter

ts
interface Greeter {
  greet(name: string): string
}

const englishGreeter: Greeter = {
  greet: (name) => `Hello, ${name}!`,
}

const chineseGreeter: Greeter = {
  greet: (name) => `你好,${name}!`,
}

let greeter: Greeter = englishGreeter

function app() {
  console.log('[App]', greeter.greet('Alex'))
}

app()

greeter = chineseGreeter
app()

运行结果如下:

text
[App] Hello, Alex!
[App] 你好,Alex!

这里的切换其实只是一次普通赋值:

ts
greeter = chineseGreeter

对于当前需求,这个实现已经足够清楚。它也提醒我们一件事:框架并不会自动让程序变得更好。如果系统只有一个简单、稳定的能力,那么直接使用变量往往就是最合适的方案。

3. Cordis:先把能力放进 Context

现在我们不增加任何新功能,只是用 Cordis 重写刚才同样的程序。这样做不是为了证明 Cordis 更简单,而是为了先认识三个最基本的 Runtime 概念:

text
Context  Service  Plugin

先把 Greeter 改写成一个 Service

ts
import { Context, Service } from 'cordis'

abstract class Greeter extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }

  abstract greet(name: string): string
}

这里最重要的是:

ts
super(ctx, 'greeter')

在这一阶段,可以先把它理解成“向当前 Context 注册一个名为 greeter 的能力”。于是原来只是一个独立变量的东西,现在变成了 Runtime 中可以被访问的一项能力:

text
Context

   └── greeter

注册完成后,其他模块不再需要直接持有实现对象,而是通过:

ts
ctx.greeter

访问这项能力。为了让 TypeScript 知道这项能力存在,还需要补一段类型声明:

ts
declare module 'cordis' {
  interface Context {
    greeter: Greeter
  }
}

4. Cordis:再通过 Plugin 安装和替换能力

接下来提供两个具体实现:

ts
class EnglishGreeter extends Greeter {
  greet(name: string) {
    return `Hello, ${name}!`
  }
}

class ChineseGreeter extends Greeter {
  greet(name: string) {
    return `你好,${name}!`
  }
}

App 也改成依赖 Context 中的能力,而不是依赖某个具体对象:

ts
function app(ctx: Context) {
  console.log('[App]', ctx.greeter.greet('Alex'))
}

这时就出现了第一个关键变化。Vanilla 版本写的是:

ts
greeter.greet('Alex')

Cordis 版本写的是:

ts
ctx.greeter.greet('Alex')

这说明 App 的依赖已经从“某个对象”变成了“Runtime 中名为 greeter 的能力”,而这正是后面支持动态安装和替换的基础。

安装方式如下:

ts
let greeterFiber = await ctx.plugin(EnglishGreeter)

app(ctx)

现阶段可以把 ctx.plugin() 简单理解为一次安装动作:

text
EnglishGreeter Plugin


      Context

        └── greeter

如果要把英文实现切换成中文实现,就先卸载旧 Plugin,再安装新 Plugin:

ts
await greeterFiber.dispose()
greeterFiber = await ctx.plugin(ChineseGreeter)

app(ctx)

5. 为什么这里一定要 dispose?

如果 EnglishGreeter 还在,就直接安装 ChineseGreeter,Cordis 会报错:

text
Error: service "greeter" has been registered at <EnglishGreeter>

原因是 greeter 这个 Service 名称已经被占用。Cordis 不允许另一个 Plugin 在没有明确清理旧能力的情况下静默覆盖它,因为那会让“这个能力是谁创建的、应该由谁回收”变得不明确。

这也是 Cordis 和普通变量赋值最早出现分歧的地方。Vanilla 中:

ts
greeter = chineseGreeter

旧值会被直接覆盖;而在 Cordis 中,一个能力的替换会经过安装、注册、运行和清理的过程。此时我们第一次隐约看到了 Runtime 的影子:Cordis 开始关心的,不只是“值是什么”,而是“能力属于谁,以及它什么时候失效”。

6. 这一篇的结论

到这里可以先得到一个阶段性结论:当系统只有一个简单能力时,Vanilla 往往比 Cordis 更直接;但 Cordis 已经提前把能力放进了 Context,并用 PluginService 建立了明确的所有权关系。换句话说,Cordis 此时看起来还不划算,并不是因为它没有价值,而是因为问题还没有发展到需要 Runtime 接手的程度。

下一篇会在这个基础上继续推进。只要再加入一个“打完招呼以后还要让其他模块知道”的需求,系统就会立刻从“一个对象怎么调用”进入“模块之间怎么协作、资源由谁清理”的问题域。