> 插件化架构的核心不在代码量——在于契约设计。EAMX 用 74 行 PluginRegistry + 79 行 LifecycleBus 总共 153 行核心代码,支撑了 11 个独立业务模块的注册、路由、权限和定时任务。本文详解 PluginManifest 契约定义和事件总线设计。
EAMX 的插件化架构遵守两条核心原则:
PluginManifest 接口,不关心其他插件的存在这与微内核架构一致:内核越小,系统越稳定。
``typescript<br>interface PluginManifest {<br> id: string; // 唯一标识,如 "equipment"<br> name: string; // 显示名称<br> version: string; // 语义化版本<br> description?: string;<br> dependencies?: string[]; // 插件依赖(加载顺序约束)<br> tier: 0 | 1 | 2 | 3; // 插件分层<br> // 生命周期钩子<br> lifecycleHandlers?: Partial<LifecycleHandlerMap>;<br> // 资源注册<br> createRouter?: () => Router; // Express Router 工厂<br> routePrefix?: string; // 路由前缀,如 "/api/equipment"<br> permissions: PermissionDefinition[]; // CASL 权限定义<br> navItems?: NavItem[]; // 前端导航项<br> cronJobs?: CronJobDefinition[]; // 定时任务<br>}<br>``
Tier 分层设计:
| Tier | 说明 | 示例 |<br>|---|---|---|<br>| Tier 0 | 平台核心(不可卸载) | auth、tenant |<br>| Tier 1 | 基础业务(EAM 基础功能) | equipment、spare-part |<br>| Tier 2 | 业务增强(可选) | pm-plan、purchase |<br>| Tier 3 | 实验/社区(可热拔插) | mockup、sandbox |
Tier 决定了卸载时的影响范围和降级策略。
```typescript<br>class PluginRegistry {<br> private plugins = new Map<string, PluginManifest>();
register(plugin: PluginManifest): void {<br> // ① 幂等检查:不允许重复注册<br> if (this.plugins.has(plugin.id))<br> throw new Error(Plugin "${plugin.id}" is already registered);
// ② 依赖检查:所有 dependencies 必须已注册<br> for (const dep of plugin.dependencies ?? []) {<br> if (!this.plugins.has(dep))<br> throw new Error(Plugin "${plugin.id}" depends on "${dep}" which is not registered);<br> }
// ③ 自动注册生命周期钩子<br> if (plugin.lifecycleHandlers) {<br> for (const [event, handler] of Object.entries(plugin.lifecycleHandlers)) {<br> lifecycle.on(event as LifecycleEvent, handler);<br> }<br> }
this.plugins.set(plugin.id, plugin);<br> }
// 将所有已注册插件的路由挂载到父 Router<br> mountAll(router: Router): void {<br> for (const [, plugin] of this.plugins) {<br> if (!plugin.createRouter) continue;<br> router.use(plugin.routePrefix ?? "/", plugin.createRouter());<br> }<br> }
// 聚合所有插件的权限定义<br> getAllPermissions(): PermissionDefinition[] {<br> return [...this.plugins.values()].flatMap(p => p.permissions);<br> }
// 聚合所有导航项,按 order 排序<br> getAllNavItems(): NavItem[] {<br> return [...this.plugins.values()]<br> .flatMap(p => p.navItems ?? [])<br> .sort((a, b) => (a.order ?? 99) - (b.order ?? 99));<br> }<br>}<br>```
74 行,6 个方法,没有状态机、没有复杂配置——这正是"核心最小化"的体现。
```typescript<br>class LifecycleBus {<br> private handlers = new Map<string, AnyHandler[]>();
// 观察型事件:并行执行,单个失败不阻断主流程<br> async emit<T>(event: string, payload: T, ctx: RequestContext): Promise<void> {<br> const handlers = this.handlers.get(event) ?? [];<br> const results = await Promise.allSettled(<br> handlers.map(h => h({ payload, ctx }))<br> );<br> // 失败仅打日志,不抛异常<br> }
// 拦截型事件:串行执行,支持修改 payload 或中断(AbortError)<br> async intercept<T>(event: string, payload: T, ctx: RequestContext): Promise<T> {<br> let current = { ...payload };<br> for (const handler of handlers) {<br> await handler({<br> payload: current, ctx,<br> intercept: (modified) => { current = { ...current, ...modified }; },<br> abort: (reason) => { abortReason = reason; },<br> });<br> if (abortReason) throw new AbortError(abortReason);<br> }<br> return current;<br> }<br>}<br>```
双模式设计解决了两类核心需求:
| 模式 | 用例 | 行为 |<br>|---|---|---|<br>| emit(观察) | 通知、日志、缓存失效 | 并行,失败不阻塞,fire-and-forget |<br>| intercept(拦截) | 审批流、数据校验、权限注入 | 串行,可修改 payload,可 AbortError 中断 |
典型拦截场景:设备创建前 → approval 插件检查审批策略 → 满足条件继续 / 不满足则 abort("需要审批")。
``typescript<br>// approval-plugin.ts(简化版)<br>export const approvalPlugin: PluginManifest = {<br> id: "approval",<br> name: "审批流",<br> version: "1.0.0",<br> tier: 1,<br> dependencies: ["auth"],<br> permissions: [<br> { action: "approve", subject: "WorkOrder" },<br> { action: "approve", subject: "PurchaseRequest" },<br> ],<br> routePrefix: "/api/approval",<br> createRouter: () => {<br> const router = Router();<br> router.get("/pending", listPendingApprovals);<br> router.post("/:id/approve", approveRequest);<br> router.post("/:id/reject", rejectRequest);<br> return router;<br> },<br> lifecycleHandlers: {<br> "equipment:beforeCreate": async ({ payload, abort }) => {<br> if (payload.value > 100000) {<br> // 超过 10 万元需要审批<br> abort("设备价值超过10万元,需要主管审批");<br> }<br> },<br> },<br>};<br>``
插件完全通过契约定义自己的行为——PluginRegistry 不需要知道审批流的存在。
``<br>Tier 0: auth, tenant (不可卸载)<br> ↑<br>Tier 1: equipment, spare-part, work-order, fault-report, pm-plan, approval, notification<br> ↑<br>Tier 2: purchase, inventory, report<br> ↑<br>Tier 3: mockup (实验性)<br>``
启动时按依赖拓扑排序加载,确保 Tier 0 最先注册。Tier 3 插件加载失败不影响系统启动。
| 文件 | 行数 | 职责 |<br>|---|---|---|<br>| packages/core/src/plugin-registry.ts | 74 | 插件注册、路由挂载、权限/导航聚合 |<br>| packages/core/src/lifecycle-bus.ts | 79 | emit/拦截事件总线 |<br>| packages/core/src/types.ts | — | PluginManifest 接口定义 |
下一篇:CASL 同构权限系统——15 个 Actions × 26 个 Subjects,前后端零差异鉴权。
作者:白杨,十余年设备资产管理从业经验,EAMX 产品负责人。