跳转到内容

快速开始

本页用十分钟带你装上 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 不修改全局对象。

安装

sh
npm install dougong

dougong 是门面包,它 re-export 三个实包。如果你只需要内核,也可以单独安装:

sh
npm install @dougongjs/core       # 六个原子、依赖图、事务、诊断
npm install @dougongjs/reactive   # Signal 值层与 observe(零依赖)
npm install @dougongjs/platform   # Manifest、权限、懒激活、HMR

TypeScript 配置

Dougong 的类型使用 Symbol.disposeAbortSignal,你的 tsconfig.json 必须包含对应的 lib:

json
{
  "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'

补上 DOMESNext.Disposable 即可。

第一个能力组合

下面是一个完整可运行的例子。provider 发布一个稳定 Service,consumer 通过 requires 声明依赖。

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

运行输出:

text
2026-08-15T00:00:00.000Z Hello, Dougong

这段代码里发生了什么

安装顺序不是启动顺序

greeter 先安装,但它依赖 clockhost.start() 时 Dougong 从 requires / provides 的声明构建依赖图,做拓扑排序,同一拓扑层内并发执行 setup,然后按逆依赖顺序停止 Instance。

你不需要手工排序,也不需要 dependsOn: ["example.clock"] 这种字符串数组——依赖关系已经在类型里了。

插件只能读它声明过的依赖

ts
definePlugin({
  name: "bad",
  setup(ctx) {
    ctx.clock.now()   // ❌ 编译错误
  },                  //    Property 'clock' does not exist on type 'PluginContext<{}>'
})

ctx 的类型是从 requires 推导出来的。没声明就没有这个属性——这是编译期错误,不会在执行阶段才得到 undefined

这条是 Dougong 和大多数插件框架最实际的区别。在依赖靠字符串或环境上下文解析的系统里,忘记声明依赖通常表现为「有时候能跑、有时候拿到 undefined」,取决于加载顺序。

host.get() 是给应用代码用的,不是给插件用的

ts
host.get(GREETER)     // ✓ 应用代码跨越 Host 边界读取能力
ctx.get(GREETER)     // ✗ 不存在这个方法

插件之间通过 requires 建立关系,这样依赖图才是完整的。如果插件能随时用 Service Locator 拿任意能力,依赖图就不再反映真实依赖,拓扑排序和事务回滚都会失去意义。

host.get() 只在 status === "active" 时可用,否则抛 SERVICE_UNAVAILABLE

运行仓库示例

如果你想直接看更完整的场景:

sh
git clone https://github.com/Tangerg/dougong.git
cd dougong
pnpm install
pnpm examples        # 依次运行九个示例
pnpm check           # 完整验证门禁
pnpm docs:dev        # 本地启动这个文档站

十二章示例分三段递进——原子、组合、完整应用——从最小 Service 一路走到 Planet / Lynx 场景、声明式计划和模块图 HMR,全部进 CI。详见可执行示例

接下来

基于 MIT 许可证发布。