插件化架构:写一个 mini 插件宿主

专栏:TypeScript 与 Node 地基 · 第 8 / 13 篇
TypeScript插件化架构

:::info 学习目标 完成本篇后你能够:解释插件宿主的四个核心机制(注册/注入/事件/可逆副作用);亲手实现一个 60 行的 mini 插件宿主;用同样的词汇理解 Cordis 的设计。 前置:第 5~6 篇完成。预计时长:90 分钟。 :::

前六篇的所有零件——类型、模块、事件、声明合并、schema——这一篇组装成一台机器:迷你插件宿主。写完它,dsh 的 Cordis 对你将不再神秘。

插件宿主的四个核心机制

一个插件系统要回答四个问题:

  1. 插件怎么注册能力?(工具、服务、路由……)
  2. 插件怎么声明依赖?(工具没就绪,能用它的插件不该启动)
  3. 插件之间怎么通信?(不互相 import 的事件总线)
  4. 插件卸载时怎么清理?(副作用可逆)

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()

运行时全景:注册 → 使用 → 卸载

图表(ts-plugin-host.md)

与 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 版,再读工业版,看懂的就是”多出来的复杂度防的是什么”

常见踩坑

  1. 清理顺序错了——逆序清理是铁律(后注册的依赖先注册的);正序清理会用到已销毁的服务;
  2. 事件监听器忘了退订——插件卸载后监听器还挂着,被触发时操作已销毁的服务;
  3. 没有依赖就绪机制——严格版应在 get 不到依赖时让插件进入等待(Cordis 的 PENDING)。

随堂练习(带验收标准)

  1. 跑通 mini-host + greet 插件,dispose() 后再调 get('greet') 应返回 undefined;
  2. 写第二个插件 time-plugin:启动定时器每秒发 tick 事件,并在 dispose 时 clearInterval。验收:dispose 后 tick 停止;
  3. 挑战:给 get 加依赖等待——服务不存在时返回一个”就绪后自动 resolve”的 Promise(提示:存 resolver 列表,provide 时逐个 resolve)。这就是 Cordis 依赖门控的最小实现。

← 返回文章列表