- 低代码
- 后端
- 前端
- 插件系统
【免费下载链接】tego
Tego 是一个可插拔的 Node.js 框架,用于构建可定制的开发平台。它使开发者能够创建自己的无代码/低代码系统或事件驱动型应用程序,且框架核心侧重于稳定性与对不同运行环境的适配。
Tego 是一个可插拔的 Node.js 框架,其依赖注入(DI)容器位于独立包@tachybase/di(源码在 packages/di/),是整个框架插件化、低代码能力的"底座"。本文不堆代码,而是带你从核心源码的设计思想出发,快速理解这个容器"为什么这样写",以及@Service、@Inject这些装饰器背后到底发生了什么。
一、为什么 Tego 需要一个依赖注入容器?
一句话:解耦。Tego 的核心功能由一个个插件组成,插件之间、插件与框架之间互相需要引用(比如控制器需要拿到 Application 实例、Action 注册表)。如果每个插件都去new别人,就会形成硬编码的依赖网,改一处崩一片。
DI 容器的做法是:
- 服务自己注册:每个可注入的类通过
@Service()装饰器声明"我可以被注入"; - 消费方只声明"我要什么":通过
@Inject()指定要哪个服务,由容器在实例化时自动填好; - 实例由容器统一创建和管理:单例、生命周期、销毁都由容器负责。
这样插件之间只面向"标识符"(类名、字符串或 Token)编程,完全解耦。
二、核心概念:先认识 4 个关键角色 🧩
| 角色 | 所在文件 | 职责 |
|---|---|---|
ContainerInstance | container-instance.class.ts | 容器主体,负责注册、查找、创建、销毁服务 |
Container | 同上(文件末尾别名) | 默认全局容器,日常最常用 |
ContainerRegistry | container-registry.class.ts | 容器登记处,管理所有按 id 创建的容器 |
Token | token.class.ts | 类型化服务标识符,适合"只有接口没有类"的场景 |
其中Container就是ContainerInstance.default的别名。容器采用惰性单例:第一次访问ContainerInstance.default时才创建并注册进 Registry(见 container-instance.class.ts#L43-L49)。这个设计避免了模块加载顺序问题——你不必担心"谁先 import 谁"。
2.1 服务的三种标识方式
容器的get/set接收的标识符(ServiceIdentifier)有三种,对应不同使用场景(定义见 service-identifier.type.ts):
- 类本身:
container.set({ type: MyService })—— 最常用; - 字符串 id:
container.set({ id: 'actions', type: X })—— 一个 id 下可挂多个实例; - 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):
- 为每次注册生成一个内部掩码 Token(如
MultiMaskToken-xxx); - 把真实服务改挂到这个掩码 Token 下,当作普通单实例服务处理;
- 原 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):
- 先看本容器的元数据;
- 若默认容器里有
singleton同名服务,则使用默认容器的那份; - 子容器第一次向"父"借用服务时,先写入一个
EMPTY_VALUE占位再创建——这一手是为了防止循环依赖导致无限递归查找,是很实用的防御性设计; - 都没有则抛出
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 个设计思想一图流 📌
- 注册与实例化分离—— 元数据档案 +
EMPTY_VALUE哨兵,惰性创建、按需缓存; - 作用域显式化——
singleton/container/transient三档,单例自动归拢到默认容器; - 声明式 API——
@Service定义即注册,@Inject只声明意图,实例化细节全部隐藏; - 复杂问题入口消化——
multiple用掩码 Token 归约成单实例逻辑,核心路径保持简单; - 防御性细节—— 子容器借用先占位防空循环,专门错误类便于排查,容器销毁后一切调用立即失败。
理解了这个容器,再看 Tego 的插件加载、资源路由、Action 系统,你会发现它们都遵循同一套"面向标识符、由容器装配"的思路。建议下一步顺着 packages/core/src/plugin-manager/ 里对Container的调用继续读,把"插件如何被装配"整条链路走通。
- 低代码
- 后端
- 前端
- 插件系统
【免费下载链接】tego
Tego 是一个可插拔的 Node.js 框架,用于构建可定制的开发平台。它使开发者能够创建自己的无代码/低代码系统或事件驱动型应用程序,且框架核心侧重于稳定性与对不同运行环境的适配。
相关推荐
RuoYi-Vue-Plus 企业级多租户架构深度解析与设计模式实践
RuoYi Vue Plus 企业级多租户架构深度解析与设计模式实践 RuoYi Vue Plus作为Dromara开源社区的企业级Java快速开发平台,基于S
后端企业应用认证鉴权彻底搞懂Spring IoC:从核心容器到依赖注入实战
彻底搞懂Spring IoC:从核心容器到依赖注入实战 你还在为Spring框架中晦涩的IoC(Inversion of Control,控制反转)容器和依赖注
后端Web框架依赖注入告别面条代码:Express依赖注入(DI)设计模式实战指南
告别面条代码:Express依赖注入 DI 设计模式实战指南 Express作为一款轻量级Node.js Web框架,以其灵活简洁的特性深受开发者喜爱。然而随着
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考