快速开始
本页用十分钟带你装上 Dougong、写出第一个能力组合,并理解它和普通 DI 容器的关键区别。
环境要求
| 项目 | 要求 |
|---|---|
| Node.js | ≥ 22 |
| JavaScript 运行时 | 需提供 ES2024 标准能力,包括 Promise.withResolvers() |
| TypeScript | ≥ 5.5(如果使用 TypeScript) |
浏览器 / WebView 运行时
Promise.withResolvers() 需要 Safari 17.4+(macOS 14.4+)、Chrome 119+、Firefox 121+。 如果你在 Electron、Tauri 或 Wails 里使用系统 WebView,请先确认目标系统版本。
显式 .dispose() 不要求运行时提供 Explicit Resource Management。using / await using 还要求 Symbol.dispose / Symbol.asyncDispose;没有原生实现时,由应用在导入 Dougong 前显式 polyfill。Dougong 不修改全局对象。
安装
npm install dougongdougong 是门面包,它 re-export 三个实包。如果你只需要内核,也可以单独安装:
npm install @dougongjs/core # 六个原子、依赖图、事务、诊断
npm install @dougongjs/reactive # Signal 值层与 observe(零依赖)
npm install @dougongjs/platform # Manifest、权限、懒激活、HMRTypeScript 配置
Dougong 的类型使用 Symbol.dispose 和 AbortSignal,你的 tsconfig.json 必须包含对应的 lib:
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024", "DOM", "DOM.Iterable", "ESNext.Disposable"],
"moduleResolution": "Bundler",
"strict": true
}
}少了会怎样
只写 "lib": ["ES2024"] 会得到一串指向 node_modules 的报错,看起来像库坏了:
Property 'dispose' does not exist on type 'SymbolConstructor'
Cannot find name 'AbortSignal'补上 DOM 和 ESNext.Disposable 即可。
第一个能力组合
下面是一个完整可运行的例子。provider 发布一个稳定 Service,consumer 通过 requires 声明依赖。
import { createHost, definePlugin, service } from "dougong"
interface Clock {
now(): Date
}
interface Greeter {
greet(name: string): string
}
// 1. Contract 是一个冻结的身份令牌,把类型和字符串 ID 绑在一起
const CLOCK = service<Clock>("example/clock")
const GREETER = service<Greeter>("example/greeter")
// 2. provider:在 provides 里声明,从 setup 返回实现
const clock = definePlugin({
name: "example.clock",
provides: { clock: CLOCK },
setup: () => ({ clock: { now: () => new Date() } }),
})
// 3. consumer:在 requires 里声明,从 ctx 读取
const greeter = definePlugin({
name: "example.greeter",
requires: { clock: CLOCK },
provides: { greeter: GREETER },
setup: (ctx) => ({
greeter: {
greet: (name) => `${ctx.clock.now().toISOString()} Hello, ${name}`,
},
}),
})
const host = createHost({ name: "hello" })
host.install(greeter) // 注意:先装 consumer
host.install(clock) // 后装 provider —— 顺序无所谓
await host.start()
console.log(host.get(GREETER).greet("Dougong"))
await host.stop()运行输出:
2026-08-15T00:00:00.000Z Hello, Dougong这段代码里发生了什么
安装顺序不是启动顺序
greeter 先安装,但它依赖 clock。host.start() 时 Dougong 从 requires / provides 的声明构建依赖图,做拓扑排序,同一拓扑层内并发执行 setup,然后按逆依赖顺序停止 Instance。
你不需要手工排序,也不需要 dependsOn: ["example.clock"] 这种字符串数组——依赖关系已经在类型里了。
插件只能读它声明过的依赖
definePlugin({
name: "bad",
setup(ctx) {
ctx.clock.now() // ❌ 编译错误
}, // Property 'clock' does not exist on type 'PluginContext<{}>'
})ctx 的类型是从 requires 推导出来的。没声明就没有这个属性——这是编译期错误,不会在执行阶段才得到 undefined。
这条是 Dougong 和大多数插件框架最实际的区别。在依赖靠字符串或环境上下文解析的系统里,忘记声明依赖通常表现为「有时候能跑、有时候拿到 undefined」,取决于加载顺序。
host.get() 是给应用代码用的,不是给插件用的
host.get(GREETER) // ✓ 应用代码跨越 Host 边界读取能力
ctx.get(GREETER) // ✗ 不存在这个方法插件之间通过 requires 建立关系,这样依赖图才是完整的。如果插件能随时用 Service Locator 拿任意能力,依赖图就不再反映真实依赖,拓扑排序和事务回滚都会失去意义。
host.get() 只在 status === "active" 时可用,否则抛 SERVICE_UNAVAILABLE。
运行仓库示例
如果你想直接看更完整的场景:
git clone https://github.com/Tangerg/dougong.git
cd dougong
pnpm install
pnpm examples # 依次运行九个示例
pnpm check # 完整验证门禁
pnpm docs:dev # 本地启动这个文档站十二章示例分三段递进——原子、组合、完整应用——从最小 Service 一路走到 Planet / Lynx 场景、声明式计划和模块图 HMR,全部进 CI。详见可执行示例。
接下来
- 想理解模型 → 核心概念:六个原子各自解决什么,为什么 ExtensionPoint 不能用 Service 替代
- 想直接写代码 → 编写插件:配置校验、可选依赖、失败处理
- 关心资源泄漏 → 生命周期与资源
- 需要精确语义 → Core API 规范