插件化架构:写一个 mini 插件宿主
专栏:TypeScript 与 Node 地基 · 第 8 / 13 篇:::info 学习目标 完成本篇后你能够:解释插件宿主的四个核心机制(注册/注入/事件/可逆副作用);亲手实现一个 60 行的 mini 插件宿主;用同样的词汇理解 Cordis 的设计。 前置:第 5~6 篇完成。预计时长:90 分钟。 :::
前六篇的所有零件——类型、模块、事件、声明合并、schema——这一篇组装成一台机器:迷你插件宿主。写完它,dsh 的 Cordis 对你将不再神秘。
插件宿主的四个核心机制
一个插件系统要回答四个问题:
- 插件怎么注册能力?(工具、服务、路由……)
- 插件怎么声明依赖?(工具没就绪,能用它的插件不该启动)
- 插件之间怎么通信?(不互相 import 的事件总线)
- 插件卸载时怎么清理?(副作用可逆)
60 行实现
// mini-host.ts —— Cordis 的最小版
type Disposer = () => void
type Plugin = (host: MiniHost) => void | Disposer
export class MiniHost {
private services = new Map<string, unknown>()
private listeners = new Map<string, Set<Function>>()
private disposers: Disposer[] = []
// ① 注册服务:记录 + 返回撤销函数
provide<T>(name: string, service: T): Disposer {
this.services.set(name, service)
this.emit(`provided:${name}`, service)
const undo = () => this.services.delete(name)
this.disposers.push(undo)
return undo
}
// ② 读取服务(依赖就绪性由插件自己的启动顺序保证;严格版见踩坑 3)
get<T>(name: string): T | undefined {
return this.services.get(name) as T | undefined
}
// ③ 事件总线:发布-订阅,返回退订函数
on(event: string, fn: Function): Disposer {
if (!this.listeners.has(event)) this.listeners.set(event, new Set())
this.listeners.get(event)!.add(fn)
const undo = () => this.listeners.get(event)?.delete(fn)
this.disposers.push(undo)
return undo
}
emit(event: string, ...args: unknown[]): void {
this.listeners.get(event)?.forEach(fn => fn(...args))
}
// ④ 注册"副作用":任何需要清理的东西都登记在这
effect(cleanup: Disposer): void {
this.disposers.push(cleanup)
}
// 卸载:按注册逆序执行全部清理
dispose(): void {
[...this.disposers].reverse().forEach(d => d())
this.disposers = []
this.services.clear()
this.listeners.clear()
}
}
用插件写一个”工具”试试——插件就是接收 host 的函数,通过注册能力并返回清理逻辑来参与系统:
// plugin-greet.ts —— 一个插件
import { MiniHost } from './mini-host'
export const greetPlugin = (host: MiniHost) => {
host.provide('greet', (name: string) => `你好,${name}!`)
host.on('provided:greet', () => console.log('[greet] 服务已就绪'))
}
// main.ts —— 组装
import { MiniHost } from './mini-host'
import { greetPlugin } from './plugin-greet'
const host = new MiniHost()
host.run?.(greetPlugin) ?? greetPlugin(host)
console.log(host.get<(n: string) => string>('greet')?.('caiwei'))
host.dispose()
运行时全景:注册 → 使用 → 卸载
与 dsh 的对照:Cordis 就是这套思想的工业级版本
与 dsh 的对照:Cordis 就是这套思想的工业级版本
| mini-host 的机制 | Cordis 的对应物 | 差距 |
|---|---|---|
provide(name, service) | ctx.provide / Service 基类 | Cordis 有依赖门控:依赖未就绪插件保持 PENDING |
on(event, fn) | ctx.on + 声明合并的类型化事件 | Cordis 多了 waterfall/serial 分发模式 |
disposers 逆序清理 | fiber 的 effect 栈 | Cordis 支持异步清理、生成器 effect |
get(name) | ctx.get(可探测服务是否存在) | Cordis 的服务消失会级联卸载依赖方 |
手动 dispose() | fiber 状态机 + 级联卸载 + 热重载 | Cordis 的卸载是自动、级联、可重入的 |
mini 版缺的每一行,都对应 dsh 专栏第 4 篇讲的一个真实机制——先手写 mini 版,再读工业版,看懂的就是”多出来的复杂度防的是什么”。
常见踩坑
- 清理顺序错了——逆序清理是铁律(后注册的依赖先注册的);正序清理会用到已销毁的服务;
- 事件监听器忘了退订——插件卸载后监听器还挂着,被触发时操作已销毁的服务;
- 没有依赖就绪机制——严格版应在
get不到依赖时让插件进入等待(Cordis 的 PENDING)。
随堂练习(带验收标准)
- 跑通 mini-host + greet 插件,
dispose()后再调get('greet')应返回 undefined; - 写第二个插件
time-plugin:启动定时器每秒发tick事件,并在 dispose 时clearInterval。验收:dispose 后 tick 停止; - 挑战:给
get加依赖等待——服务不存在时返回一个”就绪后自动 resolve”的 Promise(提示:存 resolver 列表,provide 时逐个 resolve)。这就是 Cordis 依赖门控的最小实现。