当前位置: 首页 > 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.jsqmd.com/news/992641/

相关文章:

  • 徕卡全站仪GeoCOM开发避坑指南:蓝牙连接超时与指令乱序的实战解决方案
  • 嵌入式开发中JTAG/EOnCE调试接口与Flash安全机制的平衡之道
  • 从建模脚本反推:手把手教你配置PyRosetta Conda环境并跑通第一个示例
  • 别再只用双线性插值了!手把手教你给Yolov5换上CARAFE上采样算子,实测小目标检测涨点明显
  • 纵剪分条线是什么?一文搞懂分条机的原理、选型与行业应用 - 速递信息
  • 别再手动传代码了!用Vercel CLI一键部署本地Nuxt.js项目(附解决HTTPS接口报错)
  • 别再死磕直接求解器了!用Python手把手实现一个简易AMG求解器(附完整代码)
  • 北京整箱老酒回收排名!批量变现商家推荐 - 光耀华夏品牌榜
  • SAP SD顾问必看:BAPI_BILLINGDOC_CREATEMULTIPLE参数详解与业务场景匹配指南
  • 如何通过Roboto字体实现全球化应用的无缝多语言排版
  • Hackintool:现代化系统诊断与硬件管理工具的技术深度解析
  • 纯C跨平台哈希表实现,含完整工程结构与可直接编译的Code::Blocks项目
  • 微信聊天记录解密终极指南:3步轻松获取你的隐私数据控制权
  • 数据的加密与解密(14:17)
  • 拆解一个完整的ROS小车项目:智行mini2的代码、通信与模块化设计思路
  • 2026 临沂防水补漏服务商口碑测评榜单|全屋渗漏维修机构优选指南 - 宅安选房屋修缮
  • 贵妇发膜评测:这些发膜到底值不值? - 热点速览
  • 柯达NVR国标GB28181接入EasyCVR踩坑记:通道数填错导致注册失败,手把手教你排查
  • 从零开始:无引导分区与全盘格式化后的纯净系统重生指南
  • Phaedra模型:科学数据压缩与量化技术解析
  • 深入解析PCA85276 LCD驱动芯片:多路复用原理、I2C配置与工程实践
  • MOOC知识概念推荐系统:AMR框架解析与实践
  • Win11在文件右键菜单中的“共享对象”出现空白图标项目的处理方式
  • 别再手动爬数据了!用Tushare Pro的Python接口,5分钟搞定A股历史行情分析
  • 3个实用技巧:用SleeperX优化你的Mac睡眠管理体验
  • 2026甄选宁波假发实体门店实测 靠谱品牌全维度解析 - 奔跑123
  • 2026衡水市权威认证贵金属回收 TOP5+黄金回收白银回收铂金回收门店地址电话推荐
  • 2026年6月最新|江苏车间净化公司推荐哪家好又不贵?高性价比TOP榜(无隐形消费 + 包验收) - 商业新知
  • 轻量级Python工具:计算两个时间序列间X→Y方向的信息传递强度
  • 深度解析Daily1%项目开发:创新引领加密投资新潮流