别再死记硬背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 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 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 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的实际容器实现要复杂得多,主要因为:
- 模块系统:依赖是按模块组织的
- 作用域隔离:请求作用域、瞬态作用域等
- 高级注入:可选注入、属性注入等
- 生命周期钩子:实例创建前后的扩展点
理解了这个简易实现后,再看NestJS的@Injectable()和@Inject()装饰器,你会发现它们本质上就是在注册和解析依赖关系。这种设计模式的威力在于:
- 可测试性:可以轻松替换依赖项进行单元测试
- 可维护性:修改实现不会影响依赖它的代码
- 可扩展性:通过装饰器可以灵活添加功能
在实际项目中,你可能会遇到需要动态创建实例的情况。这时可以扩展我们的容器,添加工厂提供者支持:
container.addProvider({ token: 'ConfigService', useFactory: () => { return { apiUrl: process.env.API_URL, timeout: parseInt(process.env.TIMEOUT || '5000') }; } });通过这个从零构建的过程,相信你已经对IoC和DI有了具象化的理解。下次当你在NestJS中使用@Injectable()时,你会清楚地知道:这不仅仅是一个装饰器,而是一套强大解耦机制的入口点。
