依赖注入原理分析
先来看看几个重要概念的解释
- 依赖倒置原则( DIP ):抽象不应该依赖实现,实现也不应该依赖实现,实现应该依赖抽象。
- 依赖注入(dependency injection,简写为 DI):依赖是指依靠某种东西来获得支持。将创建对象的任务转移给其他class,并直接使用依赖项的过程,被称为“依赖项注入”。
- 控制反转(Inversion of Control, 简写为 IoC):指一个类不应静态配置其依赖项,应由其他一些类从外部进行配置。是一种设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由框架来管理。其实现方式就是DI依赖注入
DI IOC 优点
- 减少样板代码,不需要再在业务代码中写大量实例化对象的代码了;
- 可读性和可维护性更高了,松耦合,高内聚,符合单一职责原则,一个类应该专注于履行其职责,而不是创建履行这些职责所需的对象。
依赖注入
// src/main.ts文件
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
// src/app.module.ts文件
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// src/app.controller.ts文件
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
// src/app.service.ts文件
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
Controller里并有没有看到实例化这个 AppService 的地方。这里其实是把创建这个实例对象的工作交给了nest框架,而不是 AppController 自己来创建这个对象,这就是所谓的控制反转。而把创建好的 AppService 实例对象作为 AppController 实例化时的参数传给构造器就是依赖注入了。
依赖注入的方式
- 构造器注入: 依赖关系通过 class 构造器提供;
- setter 注入:用 setter 方法注入依赖项;
- interface接口注入:依赖项提供一个注入方法,该方法将把依赖项注入到传递给它的任何客户端中。客户端必须实现一个接口,该接口的 setter 方法接收依赖;
依赖注入原理
元数据反射
ts 中的类型信息是在运行时是不存在的,那运行时是如何根据参数的类型注入对应实例的呢? 答案就是:元数据反射 反射就是在运行时动态获取一个对象的一切信息:方法/属性等等,特点在于动态类型反推导。不管是在 ts 中还是在其他类型语言中,反射的本质在于元数据。 在 TypeScript 中,反射的原理是
- 通过编译阶段对对象注入元数据信息
- 在运行阶段读取注入的元数据,从而得到对象信息。
元数据反射(Reflect Metadata) 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它。要在 ts 中启用元数据反射相关功能需要:
- npm i reflect-metadata --save。
- 在 tsconfig.json 里配置 emitDecoratorMetadata 选项为true。
- Reflect.defineMetadata(metadataKey, data, target) // target为需要挂载信息的对象
- Reflect.getMetadata(metadataKey, target)
- Reflect.getMetadata(metadataKey, instance, methodName)
使用TS装饰器添加元数据
ts 在编译后会默认存储类型元信息,需要在需存储类型的地方添加装饰器,装饰器的内容不重要。 类型元信息存储在以下有3种固定的key,这个特点对我们实现依赖注入很重要。 TypeScript 结合自身语言的特点,为使用了装饰器的代码声明注入了 3 组元数据:
- design:type:成员类型,为属性添加装饰器,ts 编译之后会在当前属性的类型会存储在 design:type
- design:paramtypes:成员所有参数类型,为类或者类 constructor 的方法参数使用了装饰器,ts 编译之后类 constructor 的所有参数的类型会存储在 design:paramtypes
- design:returntype:成员返回类型,为方法或方法参数添加装饰器,ts 编译之后当前方法的类型、函数参数类型、函数返回值类型会分别存储在 design:type、design:paramtypes、design:returntype
class FrontEnd {
frontendLang: string[] = ['js', 'html', 'css']
}
class BackEnd {
backendLang: string[] = ['java', 'mysql']
}
@Injectable
class Project {
// ts 类可以作为类型
constructor( public frontEnd: FrontEnd, public backEnd: BackEnd) {}
}
// 以上代码经 ts 编译后,会自动为 Project 添加 design:paramstypes 的元信息
// @Injectable 里面的装饰内容其实无关紧要,但是一定要有,如果没有添加装饰器,ts 编译时将不会有添加 design:paramstypes 的代码
console.log(Reflect.getMetadata('design:paramstypes', Project)) // [FrontEnd, BackEnd]
获取类型的规则:
以上例子中类型是通过 class 定义的,class 既存在于 ts 中,也存在于编译后的 js 中,所以能直接存储,对于其它情况:
- 如果类型是 interface,则存的类型是 Object 构造函数
- 如果类型是 js 的数据类型,则存的类型是其对应的构造函数(元信息就能获取FrontEnd这个构造函数
nestjs 中的依赖注入
在nestjs中也参考了angular中的依赖注入的思想,也是用module、controller和service。
@Module({
imports:[otherModule],
providers:[SaveService],
controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}
SaveService依赖的类已经记录在元信息了,内部会在初始化时取出这些构造函数,然后实例化,简单如下
依赖注入的简单实现
import 'reflect-metadata';
type Constructor<T = any> = new (...args: any[]) => T;
const Test = (): ClassDecorator => (target) => {};
class OtherService {
a = 1;
}
@Test()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args); // 这里就相当于先把类的参数类型的构造函数取出来,在外部统一new 类的时候传进去
};
Factory(TestService).testMethod(); // 1
执行逻辑:在入口文件 main.ts 中有这样一行代码
const app = await NestFactory.create(AppModule);
好读书不求甚解,了解这个思想其实就够了,更为详细的实现可阅读源码,别那么卷,看源码只为面试[狗头]