这一篇要解决的问题很简单:当系统里只有一个简单能力时,为什么普通变量已经够用;而当我们改用 Cordis 重写同样的需求时,Context、Service 和 Plugin 分别是在替代什么。为了把这个问题讲清楚,我们会先写一个完全不需要框架的版本,再用 Cordis 重写同样的逻辑,最后看两者究竟从哪里开始分叉。
1. 这一篇要解决的问题
我们的需求很简单:有英文问候,有中文问候,而且程序在运行时可以切换语言。此时系统里还没有动态依赖、资源清理或模块协作问题,所以这个起点很重要,因为它能帮助我们区分“业务本身的复杂度”和“框架带来的复杂度”。
这一篇的主线可以先压缩成下面这条路径:
一个变量
↓
Context 收住能力
↓
Service 变成可访问的名字
↓
Plugin 负责安装与替换2. Vanilla:一个变量已经足够
先定义一个 Greeter:
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()运行结果如下:
[App] Hello, Alex!
[App] 你好,Alex!这里的切换其实只是一次普通赋值:
greeter = chineseGreeter对于当前需求,这个实现已经足够清楚。它也提醒我们一件事:框架并不会自动让程序变得更好。如果系统只有一个简单、稳定的能力,那么直接使用变量往往就是最合适的方案。
3. Cordis:先把能力放进 Context
现在我们不增加任何新功能,只是用 Cordis 重写刚才同样的程序。这样做不是为了证明 Cordis 更简单,而是为了先认识三个最基本的 Runtime 概念:
Context Service Plugin先把 Greeter 改写成一个 Service:
import { Context, Service } from 'cordis'
abstract class Greeter extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
abstract greet(name: string): string
}这里最重要的是:
super(ctx, 'greeter')在这一阶段,可以先把它理解成“向当前 Context 注册一个名为 greeter 的能力”。于是原来只是一个独立变量的东西,现在变成了 Runtime 中可以被访问的一项能力:
Context
│
└── greeter注册完成后,其他模块不再需要直接持有实现对象,而是通过:
ctx.greeter访问这项能力。为了让 TypeScript 知道这项能力存在,还需要补一段类型声明:
declare module 'cordis' {
interface Context {
greeter: Greeter
}
}4. Cordis:再通过 Plugin 安装和替换能力
接下来提供两个具体实现:
class EnglishGreeter extends Greeter {
greet(name: string) {
return `Hello, ${name}!`
}
}
class ChineseGreeter extends Greeter {
greet(name: string) {
return `你好,${name}!`
}
}App 也改成依赖 Context 中的能力,而不是依赖某个具体对象:
function app(ctx: Context) {
console.log('[App]', ctx.greeter.greet('Alex'))
}这时就出现了第一个关键变化。Vanilla 版本写的是:
greeter.greet('Alex')Cordis 版本写的是:
ctx.greeter.greet('Alex')这说明 App 的依赖已经从“某个对象”变成了“Runtime 中名为 greeter 的能力”,而这正是后面支持动态安装和替换的基础。
安装方式如下:
let greeterFiber = await ctx.plugin(EnglishGreeter)
app(ctx)现阶段可以把 ctx.plugin() 简单理解为一次安装动作:
EnglishGreeter Plugin
│
↓
Context
│
└── greeter如果要把英文实现切换成中文实现,就先卸载旧 Plugin,再安装新 Plugin:
await greeterFiber.dispose()
greeterFiber = await ctx.plugin(ChineseGreeter)
app(ctx)5. 为什么这里一定要 dispose?
如果 EnglishGreeter 还在,就直接安装 ChineseGreeter,Cordis 会报错:
Error: service "greeter" has been registered at <EnglishGreeter>原因是 greeter 这个 Service 名称已经被占用。Cordis 不允许另一个 Plugin 在没有明确清理旧能力的情况下静默覆盖它,因为那会让“这个能力是谁创建的、应该由谁回收”变得不明确。
这也是 Cordis 和普通变量赋值最早出现分歧的地方。Vanilla 中:
greeter = chineseGreeter旧值会被直接覆盖;而在 Cordis 中,一个能力的替换会经过安装、注册、运行和清理的过程。此时我们第一次隐约看到了 Runtime 的影子:Cordis 开始关心的,不只是“值是什么”,而是“能力属于谁,以及它什么时候失效”。
6. 这一篇的结论
到这里可以先得到一个阶段性结论:当系统只有一个简单能力时,Vanilla 往往比 Cordis 更直接;但 Cordis 已经提前把能力放进了 Context,并用 Plugin 和 Service 建立了明确的所有权关系。换句话说,Cordis 此时看起来还不划算,并不是因为它没有价值,而是因为问题还没有发展到需要 Runtime 接手的程度。
下一篇会在这个基础上继续推进。只要再加入一个“打完招呼以后还要让其他模块知道”的需求,系统就会立刻从“一个对象怎么调用”进入“模块之间怎么协作、资源由谁清理”的问题域。