Context:依赖容器与 Proxy
专栏:Cordis 插件框架 · 第 3 / 12 篇:::info 学习目标
完成本篇后你能够:解释 ctx.foo 属性读取背后发生了什么;用 extend/isolate/intercept 创建子上下文;说出根 Context 启动时安装了哪五个内建服务。
前置:第 2 篇完成。预计时长:50 分钟。
:::
Context 的完整实现只有 146 行(vendor/cordis/src/context.ts),但它是整个框架的门面。全文读完只需要十分钟,每一行都值得讲。
构造函数:五个内建服务
// vendor/cordis/src/context.ts:70-84(节选)
static {
Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is')
Context.prototype[Context.is as any] = true // 跨 realm 的 context 品牌标记
}
constructor() {
this[symbols.isolate] = Object.create(null)
this[symbols.intercept] = Object.create(null)
const self = new Proxy<this>(this, ReflectService.handler) // ← 关键:Proxy
this.root = self
this.fiber = new Fiber(self, {}, Object.create(null), null, () => [])
this.reflect = new ReflectService(self) // 内建 1:属性读写
this.registry = new RegistryService(self) // 内建 2:插件注册
this.events = new EventsService(self) // 内建 3:事件总线
this.logger = new LoggerService(self) // 内建 4:日志
this.fiber._disposables.clear()
return self // ← 构造器返回 Proxy 替换 this
}
三个细节:根 fiber(uid=0)在构造时同步创建,所以根上下文永远 ACTIVE;Proxy 让”读 ctx.tools”变成一次服务解析(下一篇的 reflect 层);Context.is() 用全局 Symbol 做品牌标记而非 instanceof——注释明说这是为了跨 realm 和多份 cordis 副本仍能识别。
原型链:作用域叠加的实现
ctx.foo 的读取发生了什么
ctx.foo 的读取发生了什么
ReflectService.handler 是 Proxy 的 get 处理器:普通属性直接返回;服务名走解析器(找不到且非内部属性时还会发 internal/get waterfall,给插件最后的机会提供它)。所以:
ctx.tools // → 服务解析器查 'tools' → ToolRuntime 实例
ctx.on(...) // → mixin 转发到 ctx.events.on
ctx.plugin(...) // → mixin 转发到 ctx.registry.plugin
on/plugin 这些便捷方法并不是 Context 类的方法,而是内建服务通过 mixin 混上来的(reflect.ts 的 mixin(name, keys))——服务与门面解耦。
extend:不改父级的原型继承
// vendor/cordis/src/context.ts:99-107
extend(meta = {}): this {
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
const self = Object.create(getTraceable(this, this)) // 原型指向父 ctx
for (const prop of Reflect.ownKeys(meta)) {
Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
}
...
return self
}
extend 用 Object.create 创建子上下文:子读父的一切,子的自有属性遮蔽父级,父级完全不受影响。这就是 dsh 每个 agent 一个 scope(第 10 篇)的实现地基——隔离不靠复制,靠原型链。
isolate(name) 与 intercept(name, config) 都是 extend 的封装:它们创建的 shadow 对象(isolate 映射 / intercept 映射)也用 Object.create(父的映射) 串联——沿原型链向上查找,近处的标签遮蔽远处。用原型链表达”作用域叠加”,是全框架最优雅的一笔。
fiber:ctx 的”所有者”
每个 ctx 都带 ctx.fiber——拥有它的插件运行实例。根 ctx 的 fiber 是特殊的 uid=0 fiber;插件 ctx 的 fiber 由 ctx.plugin() 创建(parent.extend({ fiber: this }),fiber.ts 构造器)。ctx.fiber.state、ctx.fiber.dispose()、ctx.effect() 全是它的能力(第 8~9 篇)。
三种创建子上下文的方式
| 方法 | 作用 | dsh 里的用例 |
|---|---|---|
extend(meta) | 加自有属性(如 { agent }) | agent.ctx 携带 agent 引用 |
isolate(name, label) | 服务名解析到独立标签 | 多个会话各自一套 shell 实现 |
intercept(name, config) | 给某服务合并专属配置 | preset 给 tools 合并配置 |
三者都返回新 ctx 且不修改父级——叠加任意多层。
常见踩坑
- 把 ctx 当普通对象展开——
{ ...ctx }会触发 Proxy 的全部读取甚至服务实例化,别这么做; - 在根 ctx 上直接 provide 后又 isolate——isolate 只影响”其子树”的解析,父级不变;
- 比较两个 ctx 用
===——应该用Context.is()(跨代理与 realm 安全)。
随堂练习(带验收标准)
- 创建根 ctx,
extend({ tag: 'child' })后验证子能读父的服务、父读不到子的自有属性; - 用
isolate('greet')创建两个子 ctx,分别 provide 不同的 greet 实现。验收:两个子 ctx 各自拿到自己的实现,父 ctx 报”服务不存在”; - 打开
vendor/cordis/src/context.ts全文精读一遍——146 行里每一个方法现在你都应该能逐行讲出来。