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

发布时间:2026/6/11 14:16:24

别再死记硬背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}); // 创建用户逻辑... } }这种写法存在三个明显问题难以测试无法在单元测试中模拟Logger难以扩展想改用FileLogger必须修改UserService难以复用Logger无法被其他服务共享配置依赖倒置原则告诉我们高层模块不应该依赖低层模块两者都应该依赖抽象。下面我们就来构建一个容器自动解决这些依赖关系。2. 实现基础IoC容器我们的迷你容器需要解决两个核心问题注册依赖项保存可用的服务实例解析依赖项自动注入所需依赖以下是基础实现class MiniContainer { private instances new Mapstring, any(); // 注册依赖项 register(token: string, instance: any) { this.instances.set(token, instance); } // 解析依赖项 resolveT(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.resolveLogger(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 Mapstring, any(); register(token: string, provider: any) { this.instances.set(token, provider); } resolveT(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.resolveUserService(UserService); userService.createUser(John); // 输出: [LOG] Creating user John4. 实现NestJS风格的依赖注入让我们更进一步模拟NestJS的核心机制。NestJS的IoC容器有三个关键设计提供者(Providers)用Injectable()装饰的类作用域(Scope)单例(SINGLETON)或瞬态(TRANSIENT)注入令牌(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 Mapstring | symbol, ProviderConfig(); private instances new Mapstring | symbol, any(); addProvider(config: ProviderConfig) { this.providers.set(config.token, config); } getT(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.getUserService(UserService); userService.createUser(Alice);5. 对比NestJS实际实现虽然我们的迷你容器已经具备了核心功能但与NestJS的实际实现相比还有差距功能点我们的实现NestJS实现依赖解析基础支持支持构造函数、属性注入生命周期管理简单单例完整生命周期(OnModuleInit等)模块化系统无模块作用域隔离循环依赖处理不支持支持forwardRef自定义提供者基础支持支持工厂、异步提供者NestJS的实际容器实现要复杂得多主要因为模块系统依赖是按模块组织的作用域隔离请求作用域、瞬态作用域等高级注入可选注入、属性注入等生命周期钩子实例创建前后的扩展点理解了这个简易实现后再看NestJS的Injectable()和Inject()装饰器你会发现它们本质上就是在注册和解析依赖关系。这种设计模式的威力在于可测试性可以轻松替换依赖项进行单元测试可维护性修改实现不会影响依赖它的代码可扩展性通过装饰器可以灵活添加功能在实际项目中你可能会遇到需要动态创建实例的情况。这时可以扩展我们的容器添加工厂提供者支持container.addProvider({ token: ConfigService, useFactory: () { return { apiUrl: process.env.API_URL, timeout: parseInt(process.env.TIMEOUT || 5000) }; } });通过这个从零构建的过程相信你已经对IoC和DI有了具象化的理解。下次当你在NestJS中使用Injectable()时你会清楚地知道这不仅仅是一个装饰器而是一套强大解耦机制的入口点。

相关新闻