# @kaokei/di — 完整 API 文档 > 轻量级 TypeScript 依赖注入库,基于 TC39 Stage 3 装饰器规范,不依赖 `reflect-metadata`,支持属性注入循环依赖。 ## 核心特点 - **不依赖 `reflect-metadata`**,无需安装该包 - **使用 TC39 Stage 3 装饰器**(不需要 `experimentalDecorators: true`,不需要 `emitDecoratorMetadata: true`) - **只支持属性注入**(Field Decorator),不支持构造函数参数注入(Parameter Decorator) - **原生支持属性注入的循环依赖**,无需借助 `@LazyInject`(这与 InversifyJS 不同) - **默认单例模式**,可通过 `inTransientScope()` 切换为瞬态模式 - **生命周期顺序与 InversifyJS 不同**(见下方说明) ## 安装 ```sh npm install @kaokei/di ``` tsconfig.json 无需特殊配置,不需要 `experimentalDecorators`,不需要 `emitDecoratorMetadata`。 --- ## 关于循环依赖 本库原生支持属性注入的循环依赖,不需要任何额外处理。 `LazyToken` 解决的是另一个问题:当类 A 和类 B 通过**类名**互相 `@Inject` 时,TypeScript 模块加载阶段会产生循环 import,导致其中一个类在装饰器执行时为 `undefined`。此时需要用 `LazyToken` 延迟求值: ```ts // a.ts import { B } from './b'; @Injectable() export class A { @Inject(new LazyToken(() => B)) // 延迟到运行时再求值,避免 import 时 B 为 undefined public b!: B; } // b.ts import { A } from './a'; @Injectable() export class B { @Inject(new LazyToken(() => A)) public a!: A; } ``` 如果通过 `Token` 实例(而非类名)来绑定服务,则不存在循环 import 问题,也不需要 `LazyToken`: ```ts // tokens.ts(独立文件,无循环依赖) export const tokenA = new Token('A'); export const tokenB = new Token('B'); // a.ts import { tokenB } from './tokens'; @Injectable() export class A { @Inject(tokenB) // 直接用 Token 实例,无循环 import 问题 public b!: B; } ``` --- ## Container ```ts const container = new Container(); ``` ### container.parent ```ts public parent?: Container; ``` 指向父级容器,`container.get` 找不到 token 时会自动向父级容器查找,一直重复直到父级容器不存在为止。 ### container.getChildren ```ts container.getChildren(): Set | undefined ``` 返回当前容器的所有直接子容器集合。若尚未创建任何子容器,返回 `undefined`,需由调用方自行判空。当子容器调用 `destroy()` 时,会自动从该集合中移除。内部通过私有属性 `_children` 存储,请勿直接访问,始终通过 `getChildren()` 获取。 ### container.get ```ts container.get(token: CommonToken, options?: GetOptions): T container.get(token: CommonToken, options: GetOptions & { optional: true }): T | void ``` 获取指定 token 对应的服务实例。options(类型 `GetOptions`)支持: - `self: true` — 只在当前容器查找 - `skipSelf: true` — 跳过当前容器,从父容器开始查找 - `optional: true` — 找不到时返回 `undefined` 而非抛出异常 ### container.tryGet ```ts container.tryGet(token: CommonToken): T | undefined ``` `get` 的便捷封装,等价于 `container.get(token, { optional: true })`。找不到绑定时返回 `undefined`,省去每次传 `optional: true` 的繁琐写法。 ```ts const service = container.tryGet(MyService); if (service) { service.doSomething(); } ``` ### container.getAsync ```ts container.getAsync(token: CommonToken, options?: GetOptions): Promise ``` `get` 的异步版本,等待 `@PostConstruct` 异步方法执行完成后再返回实例。 ```ts // get:同步返回,异步 PostConstruct 可能还未完成 const db = container.get(DatabaseService); // getAsync:等待 PostConstruct 完成 const db = await container.getAsync(DatabaseService); db.query(); // 安全,初始化已完成 ``` ### container.bind ```ts container.bind(token: CommonToken): Binding ``` 绑定 token,返回 Binding 对象用于配置服务类型。只接受 Class 或 Token 实例,不接受字符串或 Symbol。 ### container.rebind ```ts container.rebind(token: CommonToken): Binding ``` 重新绑定 token:若已有绑定,先执行 `unbind`(触发 deactivation 生命周期),再执行 `bind`,返回新的 `Binding` 对象。若尚未绑定,则直接调用 `bind`。适用于需要替换现有绑定的场景(例如测试中替换服务实现)。 ```ts container.bind(LoggerService).toSelf(); // 测试中替换为 mock container.rebind(LoggerService).toConstantValue(mockLogger); ``` ### container.unbind ```ts container.unbind(token: CommonToken): void ``` 解绑 token,触发销毁生命周期(`Container#onDeactivation` → `Binding#onDeactivation` → `@PreDestroy`)。 ### container.unbindAll ```ts container.unbindAll(): void ``` 解绑容器内所有 token。 ### container.isBound / isCurrentBound ```ts container.isBound(token: CommonToken): boolean // 含父容器 container.isCurrentBound(token: CommonToken): boolean // 仅当前容器 ``` ### container.createChild ```ts container.createChild(): Container ``` 创建子容器,自动设置 `parent` 并加入内部子容器集合(可通过 `getChildren()` 访问)。 ### container.destroy ```ts container.destroy(): void ``` 递归销毁容器及所有子容器,并从父容器的子容器集合中移除自身。**容器销毁后,任何 `get()` 或 `getAsync()` 调用都会抛出 `ContainerDestroyedError`**。InversifyJS 没有此方法。 ```ts const parent = new Container(); const child1 = parent.createChild(); parent.destroy(); // 递归销毁 parent 及 child1 ``` ### container.onActivation ```ts container.onActivation(handler: ActivationHandler): void // ActivationHandler: (ctx: Context, input: T, token?: CommonToken) => T ``` 注册容器级激活处理器,对所有 token 生效。重复注册会覆盖前一个。可选的 `token` 参数可用于区分不同 token 实现差异化逻辑。 ### container.onDeactivation ```ts container.onDeactivation(handler: DeactivationHandler): void // DeactivationHandler: (input: T, token?: CommonToken) => void ``` 注册容器级销毁处理器。重复注册会覆盖前一个。 ### Container.getContainerOf(静态方法) ```ts Container.getContainerOf(instance: object): Container | undefined ``` 从服务实例反查其所属容器。仅对通过 `to()` 或 `toSelf()` 注册并由 `container.get()` 实例化的对象有效(`toConstantValue` 和 `toDynamicValue` 不记录)。主要用于 `@LazyInject` 内部实现。 --- ## Binding `container.bind(token)` 返回 Binding 对象,支持链式调用。`Binding` 类从 `@kaokei/di` 公开导出,可用于类型标注。 ### binding.to ```ts binding.to(constructor: Newable): this ``` 将 token 关联到指定类,默认单例。 ### binding.toSelf ```ts binding.toSelf(): this ``` `to()` 的简写,要求 token 本身是一个类。 ### binding.toConstantValue ```ts binding.toConstantValue(value: T): this ``` 关联到常量值,每次 `get` 返回同一个对象。 ### binding.toDynamicValue ```ts binding.toDynamicValue(func: DynamicValue): this // DynamicValue: (ctx: Context) => T ``` 关联到工厂函数,默认单例(首次执行后缓存结果)。 ### binding.toService ```ts binding.toService(token: CommonToken): this ``` 将 tokenA 委托给 tokenB,`get(tokenA)` 等价于 `get(tokenB)`。 ### binding.inTransientScope ```ts binding.inTransientScope(): this ``` 切换为瞬态模式,每次 `get` 都创建新实例。支持 `to()`、`toSelf()`、`toDynamicValue()` 之后链式调用: ```ts container.bind(MyService).toSelf().inTransientScope(); ``` ### binding.onActivation ```ts binding.onActivation(handler: BindingActivationHandler): this // BindingActivationHandler: (ctx: Context, input: T) => T ``` 注册 token 专属激活处理器,只对当前 token 生效。由于已与特定 token 绑定,handler 不需要 `token` 参数。其返回值会作为 `Container#onActivation` 的输入参数。 ### binding.onDeactivation ```ts binding.onDeactivation(handler: BindingDeactivationHandler): this // BindingDeactivationHandler: (input: T) => void ``` 注册 token 专属销毁处理器。 ### binding.postConstructResult 反映 `@PostConstruct` 执行状态: - `UNINITIALIZED`(Symbol):尚未执行(服务还未被 `container.get` 获取过) - `undefined`:无 `@PostConstruct` 或已同步完成 - `Promise`:异步执行中 ```ts const binding = container.bind(StudentService).toSelf(); const service = container.get(StudentService); if (binding.postConstructResult instanceof Promise) { await binding.postConstructResult; // 等待异步初始化完成 } ``` --- ## Token ```ts const token = new Token('token-name'); ``` 用于标识服务的令牌对象,支持 TypeScript 类型推导。`container.bind` 只接受 Class 或 Token 实例,不接受字符串或 Symbol。 ```ts const loggerToken = new Token('logger'); container.bind(loggerToken).to(LoggerService); const logger = container.get(loggerToken); // 类型自动推导为 LoggerService ``` ## LazyToken ```ts new LazyToken(() => Token | Newable) ``` 用于解决 `@Inject` 装饰器中的循环 import 问题(装饰器在模块加载时立即执行,若两个类互相 import 会导致其中一个为 `undefined`): ```ts // a.ts @Injectable() export class A { @Inject(new LazyToken(() => B)) public b!: B; } ``` 注意:`LazyToken` 只解决模块加载时的循环 import 问题,不解决实例化时的循环依赖。本库原生支持属性注入的循环依赖。如果通过独立的 `Token` 实例绑定服务(而非直接用类名),也不需要 `LazyToken`。 --- ## 装饰器 ### @Injectable ```ts @Injectable() class MyService { ... } ``` 类装饰器,必须放在最外层(最上方)。使用了 `@Inject`、`@PostConstruct`、`@PreDestroy` 的类必须添加 `@Injectable`。仅使用 `@LazyInject` 的类不需要。使用 `decorate()` 的类也不需要(内部已模拟)。 ### @Inject ```ts @Inject(token: Token | Newable | LazyToken) ``` 属性装饰器,声明属性依赖。参数必填。 ```ts @Injectable() class DemoService { @Inject(LoggerService) private logger: LoggerService; @Inject(new Token('config')) private config: string; } ``` ### @Self / @SkipSelf / @Optional 控制容器查找范围,必须与 `@Inject` 配合使用(单独使用 `@Self`/`@Optional`/`@SkipSelf` 而不配合 `@Inject` 会抛出错误): ```ts @Injectable() class DemoService { @Self() @Inject(LoggerService) logger1!: LoggerService; // 只在当前容器查找 @SkipSelf() @Inject(LoggerService) logger2!: LoggerService; // 跳过当前容器,从父容器查找 @Optional() @Inject(LoggerService) logger3!: LoggerService; // 找不到时返回 undefined 而非抛出异常 } ``` 这 3 个装饰器来源于 Angular 的 API。 ### @PostConstruct ```ts @PostConstruct(param?: void | true | CommonToken[] | FilterFunction) ``` 方法装饰器,在依赖注入完成后自动调用(替代构造函数中无法访问注入属性的问题)。 ```ts @Injectable() class StudentService { @Inject(ConfigService) config: ConfigService; @PostConstruct() async init() { // 此时 this.config 已注入完成 await this.config.load(); } } ``` 参数控制异步等待行为: - 不传:不等待任何依赖的异步初始化,只控制在实例化之后自动执行该方法 - `true`:等待所有 `INSTANCE` 类型依赖(`to()`/`toSelf()` 绑定)的 `@PostConstruct` 完成 - `CommonToken[]`:等待指定 token 的 `@PostConstruct` 完成 - `FilterFunction`:自定义过滤逻辑 **注意**:`@PostConstruct(true)` 始终是异步执行的(即使没有需要等待的依赖),因为底层使用 `Promise.all([]).then(...)` 实现。`@PostConstruct(true)` 只等待 `INSTANCE` 类型依赖,不等待 `toConstantValue` 和 `toDynamicValue` 绑定的依赖。 继承行为:沿继承链向上查找,执行第一个找到的 `@PostConstruct` 方法,找到即停止。 循环依赖中的限制:`@PostConstruct(true)` 只能由"先被解析的一方"等待"后被解析的一方",反向等待会触发 `PostConstructError`(因为被等待方的 `postConstructResult` 还是 `UNINITIALIZED`)。 ### @PreDestroy ```ts @PreDestroy() ``` 方法装饰器,在 `container.unbind` 时自动调用,用于清理资源。 ## @LazyInject / createLazyInject ```ts @LazyInject(token: GenericToken, container?: Container) ``` 属性装饰器,提供两个核心特性: **特性一:延迟初始化** — 不访问属性就不触发依赖解析,比 `@Inject` 更懒 ```ts class A { @LazyInject(B) public declare b: B; } container.bind(A).toSelf(); container.bind(B).toSelf(); const a = container.get(A); // 此时 a.b 尚未初始化 console.log(a.b); // 首次访问时才触发初始化 ``` **特性二:支持第三方类注入** — 宿主类不在 DI 体系内时(如 React 类组件由框架实例化),通过显式传入 `container` 完成注入 ```ts const container = new Container(); container.bind(MyService).toSelf(); class MyReactComponent extends React.Component { @LazyInject(MyService, container) // 显式指定 container service!: MyService; } ``` **createLazyInject**:绑定固定 container,避免重复传参: ```ts const LazyInject = createLazyInject(container); class A { @LazyInject(B) b!: B; @LazyInject(C) c!: C; } ``` **使用限制**: - `@LazyInject` 不能与 `@Self`/`@Optional`/`@SkipSelf` 配合使用(实现机制完全不同,彼此独立) - 不传 `container` 参数的自动查找,要求宿主类必须通过 `to()` 或 `toSelf()` 注册到容器并通过 `container.get()` 获取实例;否则抛出 `ContainerNotFoundError` --- ## decorate 无装饰器语法时的替代方案(适用于 JavaScript 或第三方类): ```ts function decorate(decorator: any, target: any, key: string): void; ``` ```ts // 等价于 @Injectable() + @Inject(B) 在属性 b 上 decorate(Inject(B), A, 'b'); // 多个装饰器 decorate([Inject(B), Self(), Optional()], A, 'b'); // 方法装饰器 decorate(PostConstruct(), A, 'init'); ``` 使用 `decorate()` 的类不需要手动添加 `@Injectable`(内部已模拟)。 支持的装饰器(只使用 `context.name` 和 `context.metadata`): | 装饰器 | 类型 | |--------|------| | `@Inject(token)` | 属性装饰器 | | `@Self()` | 属性装饰器 | | `@SkipSelf()` | 属性装饰器 | | `@Optional()` | 属性装饰器 | | `@PostConstruct()` | 方法装饰器 | | `@PreDestroy()` | 方法装饰器 | 不支持的装饰器(依赖 `context.addInitializer`,`decorate` 调用后会抛出错误): | 装饰器 | 原因 | |--------|------| | `@LazyInject(token)` | 依赖 `addInitializer` 在实例上定义 getter/setter | --- ## 元数据 API 本库导出三个底层元数据操作函数,供高级用例直接读写类元数据: ```ts import { defineMetadata, getOwnMetadata, getMetadata } from '@kaokei/di'; ``` ### defineMetadata ```ts function defineMetadata(target: CommonToken, metadata: Record): void; ``` 将 `metadata` 对象关联到 `target`。由 `@Injectable` 和 `decorate()` 在内部调用,将 Stage 3 装饰器写入的 `context.metadata` 存储到全局 CacheMap。每次调用都会替换原有条目,并自动使 `getInjectedProps` 的缓存失效。 ### getOwnMetadata ```ts function getOwnMetadata(key: string, target: CommonToken): unknown; ``` 获取 `target` **自身**的元数据值,不沿继承链向上查找。等价于 `Reflect.getOwnMetadata(key, target)`。 ### getMetadata ```ts function getMetadata(key: string, target: CommonToken): unknown; ``` 获取元数据值,沿继承链向上查找直到找到为止。等价于 `Reflect.getMetadata(key, target)`。 | | `getOwnMetadata` | `getMetadata` | |---|---|---| | 查找范围 | 只查 target 自身 | 沿原型链向上查找 | | 子类覆盖父类 | 不会看到父类值 | 优先返回子类值 | | 适用场景 | 判断某类是否自身定义了某元数据 | 获取实际生效的元数据值 | --- ## 装饰器速查表 | 装饰器 | 类型 | 支持无括号调用 | 支持有括号调用 | 需要配合 `@Injectable` | 需要配合 `@Inject` | 可在 `decorate()` 中使用 | |--------|------|:-----------:|:-----------:|:------------------:|:---------------:|:---------------------:| | `@Injectable` | 类装饰器 | ✗ | ✓ | — | ✗ | ✗ | | `@Inject` | 属性装饰器 | ✗ | ✓ | ✓ | — | ✓ | | `@Self` | 属性装饰器 | ✗ | ✓ | ✓ | ✓ | ✓ | | `@SkipSelf` | 属性装饰器 | ✗ | ✓ | ✓ | ✓ | ✓ | | `@Optional` | 属性装饰器 | ✗ | ✓ | ✓ | ✓ | ✓ | | `@PostConstruct` | 方法装饰器 | ✗ | ✓ | ✓ | ✗ | ✓ | | `@PreDestroy` | 方法装饰器 | ✗ | ✓ | ✓ | ✗ | ✓ | | `@LazyInject` | 属性装饰器 | ✗ | ✓ | ✗ | ✗ | ✗ | --- ## 错误类 共 8 个错误类,继承关系: ``` Error └── BaseError ├── BindingNotFoundError — get 时找不到 token 绑定(含依赖链路信息) ├── BindingNotValidError — bind 后未调用 to/toSelf 等方法 ├── DuplicateBindingError — 同一 token 重复 bind ├── ContainerNotFoundError — @LazyInject 无法反查容器 ├── ContainerDestroyedError — 容器已销毁后调用 get/getAsync └── CircularDependencyError — activation 阶段循环依赖 └── PostConstructError — @PostConstruct 内部抛出异常 ``` `BaseError` 的 `token` 属性记录触发错误的 token: ```ts import { BaseError, BindingNotFoundError, BindingNotValidError, DuplicateBindingError, ContainerNotFoundError, ContainerDestroyedError, CircularDependencyError, PostConstructError } from '@kaokei/di'; try { container.get(SomeService); } catch (error) { if (error instanceof ContainerDestroyedError) { // 容器已销毁 } else if (error instanceof BindingNotFoundError) { // 找不到绑定,error.message 含依赖链路 } else if (error instanceof PostConstructError) { // 初始化失败 } else if (error instanceof BaseError) { // 其他 DI 错误,error.token 记录触发错误的 token } } ``` --- ## 生命周期顺序 ### 激活(首次 container.get)完整流程 ``` 1. new ClassName() — 无参构造 2. Binding#onActivation — binding 级别激活(返回值传入下一步) 3. Container#onActivation — container 级别激活(返回值存入缓存) 4. 存入缓存 5. 注册实例到容器映射(_registerInstance) 6. 属性注入(_getInjectProperties,处理 @Inject) 7. @PostConstruct ``` > ⚠️ 与 InversifyJS 不同:InversifyJS 先执行 `@PostConstruct`,本库将其放在最后,确保执行时注入属性已完整。 > > 已知限制:activation handler(步骤 2、3)中不能访问注入的属性,因为属性注入(步骤 6)还未完成。 ### 销毁(container.unbind / unbindAll / destroy) ``` 1. Container#onDeactivation 2. Binding#onDeactivation 3. @PreDestroy 4. 清除缓存和绑定 ``` ### @PostConstruct 继承行为 沿继承链向上查找,执行第一个找到的 `@PostConstruct` 方法,找到即停止: - 子类和父类都有 `@PostConstruct`:只执行子类的 - 只有父类有 `@PostConstruct`:执行父类的 - 多级继承,只有祖先类有 `@PostConstruct`:执行祖先类的 --- ## 异步初始化 ```ts @Injectable() class B { id = 2; @PostConstruct() async init() { const res = await fetch('/api/b'); this.id = res.id; } } @Injectable() class A { @Inject(B) b: B; @PostConstruct(true) // 等待 B 的 PostConstruct 完成后再执行 async init() { // 此时 this.b.id 已是最终值 this.id = this.b.id + 1; } } ``` 多级链式等待同样支持(C → B → A 均使用 `@PostConstruct(true)`)。 循环依赖下的方向限制:`@PostConstruct(true)` 只能由"先被解析的一方"等待"后被解析的一方",反向等待抛出 `PostConstructError`。 --- ## 类型导出 ```ts import type { Newable, // new () => T,可实例化的类类型(无参构造) CommonToken, // Token | Newable GenericToken, // Token | Newable | LazyToken TokenType, // 从 token 推导服务类型:TokenType LazyTokenCallback, // () => CommonToken,LazyToken 的回调类型 Context, // { container: Container },activation handler 的上下文 DynamicValue, // (ctx: Context) => T,toDynamicValue 的工厂函数类型 RecordObject, // Record GetOptions, // { optional?, self?, skipSelf? },container.get 选项 Options, // GetOptions 的内部扩展类型(含 inject/token/binding/parent) ActivationHandler, // (ctx, input, token?) => T,容器级激活处理器 BindingActivationHandler, // (ctx, input) => T,Binding 级激活处理器(无 token 参数) DeactivationHandler, // (input, token?) => void,容器级销毁处理器 BindingDeactivationHandler, // (input) => void,Binding 级销毁处理器(无 token 参数) PostConstructParam, // void | true | CommonToken[] | FilterFunction InjectFunction // toDynamicValue 场景下的注入函数类型 } from '@kaokei/di'; ``` `TokenType` 使用示例: ```ts const myToken = new Token('myToken'); type ServiceType = TokenType; // string ``` `GetOptions` 各字段: - `optional`:为 `true` 时,找不到绑定返回 `undefined` 而非抛出错误 - `self`:为 `true` 时,只在当前容器中查找 - `skipSelf`:为 `true` 时,跳过当前容器,只在父容器中查找 --- ## 与 InversifyJS 的主要区别 | 特性 | @kaokei/di | InversifyJS | |------|-----------|-------------| | reflect-metadata | 不需要 | 需要 | | 装饰器规范 | TC39 Stage 3 | Legacy experimentalDecorators | | 注入方式 | 仅属性注入 | 属性注入 + 构造函数参数注入 | | 属性循环依赖 | 原生支持 | 需要第三方 lazyInject | | @PostConstruct 时机 | activation 之后(注入属性已完整) | activation 之前 | | container.rebind | 支持 | 不支持(需手动 unbind + bind)| | container.tryGet | 支持 | 不支持 | | container.destroy | 支持(递归销毁子容器) | 不支持 | | ContainerDestroyedError | 支持 | 不支持 | | onActivation/onDeactivation | 全局共享,单个回调 | 按 token 分别注册,支持多个 | | hierarchical DI 查找 | 在绑定所在容器查找依赖 | 从调用容器重新开始查找 | | 重复绑定 | 抛出 DuplicateBindingError | 支持多重绑定 | | 包体积 | 更小 | 较大 | 不支持的 InversifyJS 特性:`inRequestScope`、多重绑定(getAll/tagged/named)、中间件、模块(ContainerModule)、快照(snapshot)、Symbol 直接作为 token。 --- ## 文档链接 - 完整文档:https://di.kaokei.com - 快速开始:https://di.kaokei.com/guide/ - API 文档:https://di.kaokei.com/api/ - 示例代码:https://di.kaokei.com/examples/ - GitHub:https://github.com/kaokei/di - npm:https://www.npmjs.com/package/@kaokei/di