当前位置: 首页 > news >正文

别再死记硬背IOC和DI了!用TypeScript手写一个迷你NestJS容器,5分钟搞懂依赖注入

用TypeScript手写迷你IoC容器:5分钟透视NestJS依赖注入核心

在传统开发中,我们常常看到这样的代码:一个类直接实例化它所依赖的其他类。这种强耦合的代码就像用胶水把零件粘死的玩具,想要更换电池都得砸开外壳。而现代框架如NestJS通过**控制反转(IoC)依赖注入(DI)**机制,让组件像乐高积木一样可插拔。今天我们不谈抽象理论,直接动手用TypeScript实现一个简化版IoC容器,你会惊讶地发现:原来NestJS的魔法背后是如此简单的设计!

1. 从紧耦合代码到解耦需求

让我们先看一个典型的紧耦合案例。假设我们正在开发一个用户系统,其中UserService需要调用Logger来记录日志:

class Logger { log(message: string) { console.log(`[LOG] ${message}`); } } class UserService { private logger = new Logger(); // 直接依赖具体实现 createUser(name: string) { this.logger.log(`Creating user ${name}`); // 创建用户逻辑... } }

这种写法存在三个明显问题:

  1. 难以测试:无法在单元测试中模拟Logger
  2. 难以扩展:想改用FileLogger必须修改UserService
  3. 难以复用:Logger无法被其他服务共享配置

依赖倒置原则告诉我们:高层模块不应该依赖低层模块,两者都应该依赖抽象。下面我们就来构建一个容器,自动解决这些依赖关系。

2. 实现基础IoC容器

我们的迷你容器需要解决两个核心问题:

  1. 注册依赖项:保存可用的服务实例
  2. 解析依赖项:自动注入所需依赖

以下是基础实现:

class MiniContainer { private instances = new Map<string, any>(); // 注册依赖项 register(token: string, instance: any) { this.instances.set(token, instance); } // 解析依赖项 resolve<T>(token: string): T { const instance = this.instances.get(token); if (!instance) { throw new Error(`未找到依赖项: ${token}`); } return instance; } }

现在我们可以这样使用:

const container = new MiniContainer(); container.register('Logger', new Logger()); class UserService { private logger = container.resolve<Logger>('Logger'); // ...其他代码不变 }

这已经实现了最基本的依赖注入,但仍有改进空间。真正的IoC容器应该能:

  • 自动创建实例
  • 处理嵌套依赖
  • 支持单例/多例模式

3. 实现自动依赖解析

让我们升级容器,使其能够自动实例化类并递归解析所有依赖。首先定义装饰器来标记可注入类:

// 类装饰器 function Injectable(): ClassDecorator { return target => {}; } // 属性装饰器 function Inject(token: string): PropertyDecorator { return (target, propertyKey) => { // 元数据存储逻辑... }; }

然后增强容器实现:

@Injectable() class Logger { log(message: string) { console.log(`[LOG] ${message}`); } } @Injectable() class UserService { @Inject('Logger') private logger!: Logger; createUser(name: string) { this.logger.log(`Creating user ${name}`); } } class EnhancedContainer { private instances = new Map<string, any>(); register(token: string, provider: any) { this.instances.set(token, provider); } resolve<T>(token: string): T { // 如果已有实例直接返回 if (this.instances.has(token) && !(this.instances.get(token) instanceof Function)) { return this.instances.get(token); } // 获取提供者(类构造函数) const provider = this.instances.get(token); if (!provider) { throw new Error(`未注册的依赖项: ${token}`); } // 创建实例 const instance = new provider(); // 缓存单例 this.instances.set(token, instance); return instance; } }

现在我们可以这样使用:

const container = new EnhancedContainer(); container.register('Logger', Logger); container.register('UserService', UserService); const userService = container.resolve<UserService>('UserService'); userService.createUser('John'); // 输出: [LOG] Creating user John

4. 实现NestJS风格的依赖注入

让我们更进一步,模拟NestJS的核心机制。NestJS的IoC容器有三个关键设计:

  1. 提供者(Providers):用@Injectable()装饰的类
  2. 作用域(Scope):单例(SINGLETON)或瞬态(TRANSIENT)
  3. 注入令牌(Injection Token):可以是类、字符串或符号

以下是接近NestJS的实现:

enum ProviderScope { SINGLETON = 'SINGLETON', TRANSIENT = 'TRANSIENT' } interface ProviderConfig { token: string | symbol; useClass?: new (...args: any[]) => any; useValue?: any; scope?: ProviderScope; } class NestLikeContainer { private providers = new Map<string | symbol, ProviderConfig>(); private instances = new Map<string | symbol, any>(); addProvider(config: ProviderConfig) { this.providers.set(config.token, config); } get<T>(token: string | symbol): T { const config = this.providers.get(token); if (!config) { throw new Error(`未注册的提供者: ${token.toString()}`); } // 值提供者直接返回值 if (config.useValue) { return config.useValue; } // 单例模式且已有实例 if (config.scope === ProviderScope.SINGLETON && this.instances.has(token)) { return this.instances.get(token); } // 类提供者需要实例化 if (config.useClass) { const instance = this.instantiate(config.useClass); // 单例模式缓存实例 if (config.scope === ProviderScope.SINGLETON) { this.instances.set(token, instance); } return instance; } throw new Error(`无效的提供者配置: ${token.toString()}`); } private instantiate(cls: new (...args: any[]) => any) { // 获取类的构造函数参数类型(需要启用emitDecoratorMetadata) const paramTypes = Reflect.getMetadata('design:paramtypes', cls) || []; // 递归解析所有依赖项 const args = paramTypes.map((type: any) => { const token = type.name; // 简化处理,实际NestJS更复杂 return this.get(token); }); return new cls(...args); } }

使用示例:

// 声明服务 @Injectable() class Logger { log(message: string) { console.log(`[LOG] ${message}`); } } @Injectable() class Database { connect() { console.log('Connecting to database...'); } } @Injectable() class UserService { constructor( private logger: Logger, private database: Database ) {} createUser(name: string) { this.database.connect(); this.logger.log(`Creating user ${name}`); } } // 配置容器 const container = new NestLikeContainer(); container.addProvider({ token: Logger, useClass: Logger, scope: ProviderScope.SINGLETON }); container.addProvider({ token: Database, useClass: Database, scope: ProviderScope.SINGLETON }); container.addProvider({ token: UserService, useClass: UserService, scope: ProviderScope.SINGLETON }); // 使用服务 const userService = container.get<UserService>(UserService); userService.createUser('Alice');

5. 对比NestJS实际实现

虽然我们的迷你容器已经具备了核心功能,但与NestJS的实际实现相比还有差距:

功能点我们的实现NestJS实现
依赖解析基础支持支持构造函数、属性注入
生命周期管理简单单例完整生命周期(OnModuleInit等)
模块化系统模块作用域隔离
循环依赖处理不支持支持forwardRef
自定义提供者基础支持支持工厂、异步提供者

NestJS的实际容器实现要复杂得多,主要因为:

  1. 模块系统:依赖是按模块组织的
  2. 作用域隔离:请求作用域、瞬态作用域等
  3. 高级注入:可选注入、属性注入等
  4. 生命周期钩子:实例创建前后的扩展点

理解了这个简易实现后,再看NestJS的@Injectable()@Inject()装饰器,你会发现它们本质上就是在注册和解析依赖关系。这种设计模式的威力在于:

  • 可测试性:可以轻松替换依赖项进行单元测试
  • 可维护性:修改实现不会影响依赖它的代码
  • 可扩展性:通过装饰器可以灵活添加功能

在实际项目中,你可能会遇到需要动态创建实例的情况。这时可以扩展我们的容器,添加工厂提供者支持:

container.addProvider({ token: 'ConfigService', useFactory: () => { return { apiUrl: process.env.API_URL, timeout: parseInt(process.env.TIMEOUT || '5000') }; } });

通过这个从零构建的过程,相信你已经对IoC和DI有了具象化的理解。下次当你在NestJS中使用@Injectable()时,你会清楚地知道:这不仅仅是一个装饰器,而是一套强大解耦机制的入口点。

http://www.zskr.cn/news/1504396.html

相关文章:

  • 2026武汉洪山区香奈儿回收暗藏门道?一文让你看懂 - 逸程
  • 从建模脚本反推:手把手教你配置PyRosetta Conda环境并跑通第一个示例
  • 纵剪分条线是什么?一文搞懂分条机的原理、选型与行业应用 - 速递信息
  • 2026 临沂防水补漏服务商口碑测评榜单|全屋渗漏维修机构优选指南 - 宅安选房屋修缮
  • 柯达NVR国标GB28181接入EasyCVR踩坑记:通道数填错导致注册失败,手把手教你排查
  • 深入解析PCA85276 LCD驱动芯片:多路复用原理、I2C配置与工程实践
  • MOOC知识概念推荐系统:AMR框架解析与实践
  • 2026衡水市权威认证贵金属回收 TOP5+黄金回收白银回收铂金回收门店地址电话推荐
  • 别再手动爬数据了!用Tushare Pro的Python接口,5分钟搞定A股历史行情分析
  • 告别数组模拟!用uthash在C语言里玩转结构体哈希表(附LeetCode实战代码)
  • PCAL9554B/C I2C I/O扩展器:从原理到实战的嵌入式设计指南
  • 从H、E、F矩阵到视觉里程计:低视差与平面场景下的位姿估计实战
  • 618发膜清单:2026发膜推荐榜单好价 - 热点速览
  • BallonTranslator:如何用AI技术3小时完成传统漫画翻译3天的工作?
  • VinXiangQi:免费开源的终极象棋AI连线工具,让深度学习成为你的专属象棋教练
  • 复几何中非孤立奇点的Milnor数下界估计研究
  • QKeyMapper:Windows免费开源按键映射工具终极指南,手柄玩PC游戏的神器
  • 2026年6月PE农田灌溉管厂家推荐 - 多才菠萝
  • 从照片到三维模型:开源工具如何让3D建模变得简单高效
  • 英雄联盟Akari助手:5个智能功能让你轻松提升游戏体验
  • 山东欧克斯绿色节能建材:专业防水背衬板生产服务商 - 奔跑123
  • 料位探头开关选型全攻略:从规格到适用场景深度解析 - 品牌优选官
  • 自带报名 + 投票双功能!2026 微信报名制作平台,云众评选太省心 - 微信投票小程序
  • 重庆后汽车市场GEO优化五维实测:五家服务商实力深度对比 - 传粉科技
  • 数据的加密与解密(14:03)
  • 备考2026执业医师:我刷题用过的几款APP真实感受 - 品牌测评鉴赏家
  • 合扬领跑全城!2026 深圳古驰、戈雅包包回收五家权威机构评测公示! - 奢侈品交易观察员
  • 3分钟搞定!Windows 11 LTSC系统恢复微软商店完整指南
  • 告别卡顿!用ViewPager2和Fragment打造流畅的Android题库App(附完整源码)
  • Adobe GenP 3.0终极指南:5分钟免费激活Adobe全系列软件