实战:用 Cordis 组装一个微应用
专栏:Cordis 插件框架 · 第 12 / 12 篇:::info 学习目标
完成本篇后你能够:不借助脚手架,用 Cordis 组装一个涵盖配置、服务、事件、生命周期全部机制的微应用;用 dsh 的眼光审视自己的设计。
前置:第 211 篇完成。预计时长:23 小时。
:::
毕业项目:一个”定时提醒服务”——按配置轮询任务、提供 HTTP 查询、事件驱动日志。麻雀虽小,五个机制全用到。
项目结构
reminder/
├─ cordis.yml # 配置树(第 11 篇的 Loader 直接可用)
├─ package.json # { "type": "module", "dependencies": { "cordis": "^4" } }
└─ src/
├─ clock.ts # 插件 1:提供时钟服务
├─ store.ts # 插件 2:提供任务存储(inject: clock)
└─ main.ts # 入口:挂载 + 优雅关机
# cordis.yml —— 配置树(Loader 的条目格式)
- id: clock
name: ./src/clock.ts
config:
intervalMs: 1000
- id: store
name: ./src/store.ts
- id: http
name: ./src/http.ts
inject: [store]
三个插件
时钟服务(Service 基类 + effect 定时器):
// src/clock.ts
import { Service, Context } from 'cordis'
import { z } from 'zod'
export const Config = z.object({ intervalMs: z.number().default(1000) })
export class Clock extends Service {
static Config = Config // 配置校验:启动前自动执行(第 2 篇)
constructor(ctx: Context, config: z.infer<typeof Config>) {
super(ctx, 'clock')
ctx.effect(() => {
const timer = setInterval(() => this.ctx.emit('clock/tick', this.now()), config.intervalMs)
return () => clearInterval(timer)
}, 'clock.timer')
}
now() { return new Date().toISOString() }
}
任务存储(inject 时钟,任务到期发事件):
// src/store.ts
import { Context } from 'cordis'
import type { Clock } from './clock.ts'
export const inject = ['clock']
export function apply(ctx: Context) {
const clock = ctx.clock as Clock
const tasks: { due: number; label: string; done: boolean }[] = []
ctx.provide('tasks', {
add(label: string, dueMs: number) {
tasks.push({ due: Date.now() + dueMs, label, done: false })
},
list() { return [...tasks] },
})
ctx.effect(() => ctx.on('clock/tick', () => {
for (const t of tasks) {
if (!t.done && Date.now() >= t.due) {
t.done = true
ctx.emit('task/due', t.label) // 到期 → 发事件,日志插件来订阅
}
}
}))
}
组装与优雅关机(main.ts):new Context() → 挂载三个插件 → 注册 HTTP 查询 → 信号处理里 dispose。
验收标准(全部通过即毕业)
npx cordis(或自己的入口)启动后,添加一个 5 秒后的任务。验收:到期时看到task/due的日志;- 在
cordis.yml里把 intervalMs 改成 2000 并保存。验收:无需重启,tick 间隔变化(配置热重载生效); kill <pid>。验收:日志显示三个插件按序卸载、进程退出码 0;- 把 store 插件的
inject临时删掉并故意在 apply 第一行读ctx.clock。验收:报”服务不存在”——体会门控的价值后改回来。
对照检查:你的实现 vs dsh 的用法
| 你的实现 | dsh 里的对应物 | 差距在哪 |
|---|---|---|
cordis.yml + Loader | 162 行配置树 + patch 层叠 | dsh 多了层级优先级与热重载的 diff 更新 |
provide('tasks') | ctx.provide + Service 类 + 声明合并 | dsh 多了类型合并与作用域 |
ctx.on('clock/tick') | ctx.on + @mode 标注 | dsh 多了作用域过滤与五种分发模式 |
process.on('SIGTERM') | runProfile 的关机序列 | dsh 多了 fiber 级联卸载与超时分级 |
差距清单的每一行都对应 dsh 专栏的一篇——现在你可以带着这张表进入 DeepSeek Harness 专栏,读它的体验将是不断的”原来如此”。
结语
Cordis 专栏到此收官。回看学习路径:从第一个插件到 fiber 状态机,你掌握的不是”一个库的 API”,而是插件化系统的一整套设计语言——服务、事件、副作用、生命周期、作用域。这套语言在 dsh 里说得最完整,也会在你自己的下一个项目里继续被使用。