03 · 服务:提供方与依赖消费
目标:掌握「服务是插件提供、其他插件通过 ctx 消费的具名能力」——空间可组合性(响应式 coeffect)的落地。 对应快速入门项目:
npm run lesson:03
核心机制
- 服务占据稳定的
ctx.<key>;消费方按 key 查找而非 import 实现 → 配置层可换提供方而不改消费方(这就是ctx.tools、ctx.llm的玩法) - 提供方两部分协同:运行时
super(ctx, 'greeter')注册(注册属于 effect,卸载即移除服务)+ 编译时declare module声明合并给ctx.greeter上类型 inject声明依赖:插件保持 PENDING 直到所列服务全部就绪;加载顺序由依赖决定,与 yml 书写顺序无关inject是持续跟踪的:运行中服务消失(提供方被卸载/热替换),依赖它的插件跟着卸载;服务恢复,再自动加载
实例 A:提供方(Service 子类)
// greeter.ts
import { Service, type Context } from 'cordis'
// 编译时:声明合并把 greeter 加入 Context 接口,使 ctx.greeter 有类型。
// 不写这段运行时也能工作,只是消费方失去类型安全。
declare module 'cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
// 运行时:以名称 greeter 注册该实例
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}!`
}
}
export const name = 'greeter'
// Service 子类本身就是插件(类形态),像普通插件一样挂载
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}实例 B:消费方(inject)
// consumer.ts
import type { Context } from 'cordis'
export const name = 'consumer'
export const inject = ['greeter'] // 等 greeter 服务就绪才启动
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}# cordis.yml
- name: './greeter.ts'
- name: './consumer.ts'运行:
npm run lesson:03
# Hello, world!交换 yml 两行顺序再运行,输出不变——决定启动时机的是依赖,不是文件顺序。
可选依赖
inject 用于硬性依赖。某项功能缺失时插件仍可运行的,跳过 inject,在使用处探测:
export function apply(ctx: Context) {
// 无提供方时为 undefined,插件照样运行
const greeter = ctx.get('greeter')
console.log(greeter?.greet('maybe') ?? 'no greeter available')
}命名约定
每个应用中服务名共用一个扁平命名空间。自有服务要加有辨识度的前缀或命名空间(harness 已占用 tools、llm、agents、shell 等普通名称)。
动手练习
- 注释掉
cordis.yml里./greeter.ts一行再运行:consumer 安静保持 PENDING,不崩溃、不半运行,进程以码 0 静默退出(PENDING fiber 不会让事件循环保持活跃)。这就是「依赖恢复则复活」的静态版本 - 把
declare module块删掉再运行:运行时正常,但ctx.greeter在 IDE 里变红——体会声明合并只影响类型不影响运行时 - 写第二个消费方插件(
inject = ['greeter']),观察两个消费方都不需要知道提供方是谁
上一课:02-lifecycle-effect | 下一课:04-events