显式优于隐式
依赖写在 requires,身份写在 Contract,所有权写在 Lifetime。没有 Service Locator、环境作用域、原型链注入或 Proxy——ctx.foo 的来源永远在同一个文件里看得见。
Dougong(斗拱)解决一个具体问题:当一个应用的能力需要被拆成可独立装卸的单元时,如何让它们之间的依赖、生命周期和变更保持可推理。
它由两层组成:
外加一个互不依赖的 reactive 包,提供 Signal 值层和 observe() 组合器。
import { createHost, definePlugin, service } from "dougong"
const CLOCK = service<Clock>("app/clock")
const clock = definePlugin({
name: "app.clock",
provides: { clock: CLOCK },
setup: () => ({ clock: { now: () => new Date() } }),
})
const greeter = definePlugin({
name: "app.greeter",
requires: { clock: CLOCK }, // 依赖写在这里
setup(ctx) {
console.log(ctx.clock.now()) // 只能读声明过的依赖,否则编译错误
},
})
const host = createHost({ name: "hello" })
host.install(greeter) // 安装顺序不决定启动顺序
host.install(clock)
await host.start() // 从 Service 声明推导拓扑,同层并发启动| 适合 | 不适合 |
|---|---|
| 能力需要动态装卸、更新、回滚 | 只需要一个简单的 DI 容器 |
| 插件之间有真实依赖关系 | 插件完全独立、互不通信 |
| 半加载状态不可接受,需要事务一致性 | 长驻服务,一个模块坏了其他照跑更重要 |
| 需要向应用代码暴露可观察的 Host 状态 | 不关心执行诊断 |
| 桌面应用、编辑器内核、构建工具链 | 简单的 Web 页面 |
最后一行值得展开:Dougong 的失败模型是事务性的——一个插件 setup 失败会让整笔变更回滚。如果你的场景更需要「一个插件挂了不影响其他」的隔离性,那么 cordis 这类设计更合适。这是产品取舍,不是优劣。
文档分成三层,建议按顺序:
第一层 · 上手
第二层 · 深入
第三层 · 规范
pnpm check 十步各自守护什么如果你更喜欢读代码,可执行示例是十二章由浅入深的路径——从最小 Service 一路走到 Planet / Lynx / HMR,全部进 CI,而且「每章只增加一个新台阶」本身是一条测试。
Dougong 处于早期开发阶段(0.0.x),当前不承诺向后兼容。优先保证的是模型正确、API 一致和可执行证据完整。
运行时基线:Node.js ≥ 22;浏览器 / WebView 为 Chrome / Edge 119、Firefox 121、Safari 17.4,并提供 Promise.withResolvers()。显式 .dispose() 在全部基线上可用;using / await using 还要求运行时提供对应的 well-known Symbol 或由应用显式 polyfill。