☰
Tego 依赖注入(DI)容器实战:快速看懂核心代码的 5 个设计思想
2026/10/15 18:19:56 网站建设 项目流程
  • 低代码
  • 后端
  • 前端
  • 插件系统

【免费下载链接】tego

Tego 是一个可插拔的 Node.js 框架,用于构建可定制的开发平台。它使开发者能够创建自己的无代码/低代码系统或事件驱动型应用程序,且框架核心侧重于稳定性与对不同运行环境的适配。

项目地址:https://gitcode.com/tegojs/tego
点击查看免费下载

Tego 是一个可插拔的 Node.js 框架,其依赖注入(DI)容器位于独立包@tachybase/di(源码在 packages/di/),是整个框架插件化、低代码能力的"底座"。本文不堆代码,而是带你从核心源码的设计思想出发,快速理解这个容器"为什么这样写",以及@Service、@Inject这些装饰器背后到底发生了什么。

一、为什么 Tego 需要一个依赖注入容器?

一句话:解耦。Tego 的核心功能由一个个插件组成,插件之间、插件与框架之间互相需要引用(比如控制器需要拿到 Application 实例、Action 注册表)。如果每个插件都去new别人,就会形成硬编码的依赖网,改一处崩一片。

DI 容器的做法是:

  • 服务自己注册:每个可注入的类通过@Service()装饰器声明"我可以被注入";
  • 消费方只声明"我要什么":通过@Inject()指定要哪个服务,由容器在实例化时自动填好;
  • 实例由容器统一创建和管理:单例、生命周期、销毁都由容器负责。

这样插件之间只面向"标识符"(类名、字符串或 Token)编程,完全解耦。

二、核心概念:先认识 4 个关键角色 🧩

角色所在文件职责
ContainerInstancecontainer-instance.class.ts容器主体,负责注册、查找、创建、销毁服务
Container同上(文件末尾别名)默认全局容器,日常最常用
ContainerRegistrycontainer-registry.class.ts容器登记处,管理所有按 id 创建的容器
Tokentoken.class.ts类型化服务标识符,适合"只有接口没有类"的场景

其中Container就是ContainerInstance.default的别名。容器采用惰性单例:第一次访问ContainerInstance.default时才创建并注册进 Registry(见 container-instance.class.ts#L43-L49)。这个设计避免了模块加载顺序问题——你不必担心"谁先 import 谁"。

2.1 服务的三种标识方式

容器的get/set接收的标识符(ServiceIdentifier)有三种,对应不同使用场景(定义见 service-identifier.type.ts):

  1. 类本身:container.set({ type: MyService })—— 最常用;
  2. 字符串 id:container.set({ id: 'actions', type: X })—— 一个 id 下可挂多个实例;
  3. Token:new Token<MyInterface>('plugin')—— 面向接口编程,测试和替换实现时非常方便。

三、设计思想 1:用"元数据 + 空值哨兵"实现惰性初始化

打开核心文件 container-instance.class.ts,你会发现容器里并没有直接存服务实例,而是存了一个metadataMap(Map<ServiceIdentifier, ServiceMetadata>)。每个服务的完整"档案"定义在 service-metadata.interface.ts:id、type、factory、value、scope、multiple、eager等字段。

而"实例还没创建"这一状态,用一个私有符号标记:

export const EMPTY_VALUE = Symbol('EMPTY_VALUE');

(见 empty.const.ts)

get()时的判断逻辑非常清晰(getServiceValue,container-instance.class.ts#L354-L417):

  • 如果value不是EMPTY_VALUE,直接返回缓存实例;
  • 否则按factory 优先、类构造兜底的顺序创建,然后把实例写回元数据缓存。

思想点:注册(set)和实例化(get)彻底分离。你注册 100 个服务,实际只有被get过的那些才会new。这就是为什么"注册即创建"的写法在大型框架里是性能陷阱,而 Tego 选择了惰性。

四、设计思想 2:三种作用域,一张表看懂

scope决定了"同一个 id,能创建出几个实例",完整类型见 container-scope.type.ts:

作用域行为典型用途
singleton全局唯一,且一定注册在默认容器,所有子容器共享同一个实例全局配置、核心管理器
container每个容器内各一个实例(默认值)插件内各用一个独立实例
transient每次get都新建,不缓存无状态工具类、请求级对象

源码里有个精巧的细节(container-instance.class.ts#L163-L167):在非默认容器里set一个singleton服务时,会自动转发到默认容器去注册。这样无论谁注册,单例都保证全局唯一,用户不需要记规则。

另外还有一个eager: true选项:注册后立即创建实例(transient服务会被忽略,因为实例无法缓存管理)。适合"注册就应该开始工作"的服务。

五、设计思想 3:装饰器把"注册"变成声明式的

对使用者来说,最舒服的体验来自三个装饰器(入口在 packages/di/src/index.ts):

  • @Service():标记类可注入,并自动执行ContainerInstance.default.set(serviceMetadata)。也就是说,你只是给类加了个装饰器,注册动作在类定义时就完成了(见 service.decorator.ts#L14-L33)。还支持传参:@Service({ id: 'xxx', multiple: true, scope: 'singleton' });
  • @Inject():标记一个属性"请容器帮我填"。它不直接赋值,而是向容器登记一个 handler,等目标类实例化时再由容器调用container.get()注入(见 inject.decorator.ts#L16-L45);
  • @InjectMany():同 id 注册了多个实现时,把整组实例注入为数组(inject-many.decorator.ts)。

5.1 "multiple" 的实现:一个值得学习的技巧

multiple: true表面上允许一个 id 对应多个实例,但容器内部其实从不直接存"一组实例"。在set()中(container-instance.class.ts#L186-L205):

  1. 为每次注册生成一个内部掩码 Token(如MultiMaskToken-xxx);
  2. 把真实服务改挂到这个掩码 Token 下,当作普通单实例服务处理;
  3. 原 id 只在multiServiceIds里记录一组掩码 Token。

思想点:用"内部转换"把复杂情况归约成简单情况。getMany()只是遍历这组掩码 Token 逐个get而已。核心路径保持单一,复杂需求在入口层消化掉。

六、设计思想 4:多容器 + 默认容器分层

Tego 支持创建多个ContainerInstance(new ContainerInstance('my-id')),每个创建都会自动登记到ContainerRegistry(container-registry.class.ts#L28-L40),保证同一个 id 的容器全局只有一个。

查找时的优先级规则(get(),container-instance.class.ts#L87-L126):

  1. 先看本容器的元数据;
  2. 若默认容器里有singleton同名服务,则使用默认容器的那份;
  3. 子容器第一次向"父"借用服务时,先写入一个EMPTY_VALUE占位再创建——这一手是为了防止循环依赖导致无限递归查找,是很实用的防御性设计;
  4. 都没有则抛出ServiceNotFoundError。

错误处理也很规整:找不到服务、无法实例化、类型推断失败分别有专门错误类(packages/di/src/error/),而不是笼统地throw new Error,方便上层精准捕获。

七、实战:Tego 核心自己怎么用? 🛠

看真实使用场景比看示例更快。以插件管理器的 Web 服务为例(packages/core/src/plugin-manager/web-service.ts):

@Service() export class WebControllerService { @App() private app: Application; @Inject('actions') private actions: Map<Function, any>; }

三行装饰器就完成了:注册服务、注入 Application、按字符串 id'actions'注入 Action 注册表。而框架侧只需Container.get(WebControllerService),所有依赖自动就绪。这正是插件系统的核心收益——插件代码里找不到任何手动new或全局变量引用。

想看更多真实用法,推荐直接读测试文件,它们就是"可运行的文档":

  • 基础操作与全部作用域用例:container-instance.test.ts
  • 多实现插件系统场景:integration.test.ts#L19-L64
  • 装饰器行为:decorators.test.ts

八、小结:5 个设计思想一图流 📌

  1. 注册与实例化分离—— 元数据档案 +EMPTY_VALUE哨兵,惰性创建、按需缓存;
  2. 作用域显式化——singleton/container/transient三档,单例自动归拢到默认容器;
  3. 声明式 API——@Service定义即注册,@Inject只声明意图,实例化细节全部隐藏;
  4. 复杂问题入口消化——multiple用掩码 Token 归约成单实例逻辑,核心路径保持简单;
  5. 防御性细节—— 子容器借用先占位防空循环,专门错误类便于排查,容器销毁后一切调用立即失败。

理解了这个容器,再看 Tego 的插件加载、资源路由、Action 系统,你会发现它们都遵循同一套"面向标识符、由容器装配"的思路。建议下一步顺着 packages/core/src/plugin-manager/ 里对Container的调用继续读,把"插件如何被装配"整条链路走通。

  • 低代码
  • 后端
  • 前端
  • 插件系统

【免费下载链接】tego

Tego 是一个可插拔的 Node.js 框架,用于构建可定制的开发平台。它使开发者能够创建自己的无代码/低代码系统或事件驱动型应用程序,且框架核心侧重于稳定性与对不同运行环境的适配。

项目地址:https://gitcode.com/tegojs/tego
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询