首先 nestjs 是什么?引用其官网的原话 A progressive Node.js framework for building efficient, reliable and scalable server-side applications.
,翻译一下就是:“一个可以用来搭建高效、可靠且可扩展的服务端应用的 node 框架”。目前在 github 上有 42.4k 的 star 数,人气还是很高的。
在使用过程中会发现 nest 框架和后端同学使用的 Springboot 以及前端三大框架之一的 Angular 都有很多相似之处。没错这三个框架都有相似的设计,并都实现了依赖注入。
可能对大部分前端同学来说,依赖注入
这个词还比较陌生,本文就围绕依赖注入
这个话题,展开讨论一下依赖注入是什么?以及在 nestjs 中详细的实现过程。
先来看看几个重要概念的解释
光看上面的解释可能并不好理解?那么我们把概念和具体的代码结合起来看。
// 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!';
}
}
现在我们执行 npm start 启动服务,访问 localhost:3000 就会执行这个 AppController 类中的 getHello 方法了。我们来看 app.controller.ts 文件。可以看到构造函数的参数签名中第一个参数 appService 是 AppService 的一个实例。
constructor(private readonly appService: AppService){}
但是在代码里并有没有看到实例化这个 AppService 的地方。这里其实是把创建这个实例对象的工作交给了 nest 框架,而不是 AppController 自己来创建这个对象,这就是所谓的控制反转
。而把创建好的 AppService 实例对象作为 AppController 实例化时的参数传给构造器就是依赖注入
了。
依赖注入的实现主要有三种方式
那么 nestjs 框架用了依赖注入
和控制反转
有什么好处呢?
其实 DI
和 IoC
是实现依赖倒置原则
的具体手段。依赖倒置原则
是设计模式五大原则(SOLID)中的第五项原则,也许上面这个 AppController 的例子还看不出 DIP 有什么用,因为 DIP 也不是今天的重点,这里就不多赘述了,但是通过上面的例子我们至少能体会到以下两个优点:
我们都知道 ts 中的类型信息是在运行时是不存在的,那运行时是如何根据参数的类型注入对应实例的呢?
答案就是:元数据反射
先说反射,反射就是在运行时动态获取一个对象的一切信息:方法/属性等等,特点在于动态类型反推导。不管是在 ts 中还是在其他类型语言中,反射的本质在于元数据。在 TypeScript 中,反射的原理是通过编译阶段对对象注入元数据信息,在运行阶段读取注入的元数据,从而得到对象信息。
元数据反射(Reflect Metadata) 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它。要在 ts 中启用元数据反射相关功能需要:
npm i reflect-metadata --save
。tsconfig.json
里配置 emitDecoratorMetadata
选项为true
。Reflect.defineMetadata(metadataKey, data, target)
可以定义一个类的元数据;
Reflect.getMetadata(metadataKey, target)
,Reflect.getMetadata(metadataKey, instance, methodName)
可以获取类或者方法上定义的元数据。
TypeScript 结合自身语言的特点,为使用了装饰器的代码声明注入了 3 组元数据:
design:type
:成员类型design:paramtypes
:成员所有参数类型design:returntype
:成员返回类型import 'reflect-metadata';
class A {
sayHi() {
console.log('hi');
}
}
class B {
sayHello() {
console.log('hello');
}
}
function Module(metadata) {
const propsKeys = Object.keys(metadata);
return (target) => {
for (const property in metadata) {
if (metadata.hasOwnProperty(property)) {
Reflect.defineMetadata(property, metadata[property], target);
}
}
};
}
@Module({
controllers: [B],
providers: [A],
})
class C {}
const providers = Reflect.getMetadata('providers', C);
const controllers = Reflect.getMetadata('controllers', C);
console.log(providers, controllers); // [ [class A] ] [ [class B] ]
(new (providers[0])).sayHi(); // 'hi'
在这个例子里,我们定义了一个名为 Module 的装饰器,这个装饰器的主要作用就是往装饰的类上添加一些元数据。然后用装饰器装饰 C 类。我们就可以获取到这个参数中的信息了;
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);
};
Factory(TestService).testMethod(); // 1
这里例子就是依赖注入简单的示例,这里 Test 装饰器虽然什么都没做,但是如上所说,只要使用了装饰器,ts 就会默认给类或对应方法添加design:paramtypes
的元数据,这样就可以通过Reflect.getMetadata('design:paramtypes', target)
拿到类型信息了。
下面来看 nest 框架内部是怎么来实现的
在入口文件 main.ts 中有这样一行代码
const app = await NestFactory.create(AppModule);
在源码 nest/packages/core/nest-application.ts 找到 NestFactory.create 方法,这里用注释解释说明了与依赖注入相关的几处代码(下同)。
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
const applicationConfig = new ApplicationConfig();
// 1. 实例化 IoC 容器,这个容器就是用来存放所有对象的地方
const container = new NestContainer(applicationConfig);
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
// 2. 执行初始化逻辑,是依赖注入的核心逻辑所在
await this.initialize(module, container, applicationConfig, httpServer);
// 3. 实例化 NestApplication 类
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
// 4. 生成一个 Proxy 代理对象,将对 NestApplication 实例上部分属性的访问代理到 httpServer,在 nest 中httpServer 默认就是 express 实例对象,所以默认情况下,express 的中间件都是可以使用的
return this.createAdapterProxy<T>(target, httpServer);
}
在目录 nest/packages/core/injector/container.ts,找到了 NestContainer 类,里面有很多成员属性和方法,可以看到其中的私有属性 modules 是一个 ModulesContainer 实例对象,而 ModulesContainer 类是 Map 类的一个子类。
export class NestContainer {
...
private readonly modules = new ModulesContainer();
...
}
export class ModulesContainer extends Map<string, Module> {
private readonly _applicationId = uuid();
get applicationId(): string {
return this._applicationId;
}
}
先来看 this.initialize
方法:
private async initialize(
module: any,
container: NestContainer,
config = new ApplicationConfig(),
httpServer: HttpServer = null,
) {
// 1. 实例加载器
const instanceLoader = new InstanceLoader(container);
const metadataScanner = new MetadataScanner();
// 2. 依赖扫描器
const dependenciesScanner = new DependenciesScanner(
container,
metadataScanner,
config,
);
container.setHttpAdapter(httpServer);
const teardown = this.abortOnError === false ? rethrow : undefined;
await httpServer?.init();
try {
this.logger.log(MESSAGES.APPLICATION_START);
await ExceptionsZone.asyncRun(
async () => {
// 3. 扫描依赖
await dependenciesScanner.scan(module);
// 4. 生成依赖的实例
await instanceLoader.createInstancesOfDependencies();
dependenciesScanner.applyApplicationProviders();
},
teardown,
this.autoFlushLogs,
);
} catch (e) {
this.handleInitializationError(e);
}
}
new InstanceLoader()
实例化 InstanceLoader 类,并把刚才的 IoC 容器作为参数传入,这个类是专门用来生成需要注入的实例对象的;元数据
的工具类,而 DependenciesScanner 类是用来扫描出所有 modules 中的依赖项的。上面的 app.module.ts 中 Module 装饰器的参数中传入了controllers
、providers
等其他选项,这个 Module 装饰器的作用就是标明 AppModule 类的一些依赖项; @Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
public async scan(module: Type<any>) {
await this.registerCoreModule(); // 1. 把一些内建module添加到IoC容器中
await this.scanForModules(module); // 2. 把传入的module添加到IoC容器中
await this.scanModulesForDependencies(); // 3. 扫描当前IoC容器中所有module的依赖
this.calculateModulesDistance();
this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
}
这里所说的 module 可以理解为是模块,但并不是 es6 语言中的模块化的 module,而是 app.module.ts 中定义的类, 而 nest 内部也有一个内建的 Module
类,框架会根据 app.module.ts 中定义的 module 类去实例化一个内建的 Moudle 类。下面 addModule 方法是把 module 添加到 IoC 容器的方法,可以看到,这里针对每个 module 会生成一个 token,然后实例化内建的 Module 类,并放到容器的 modules 属性上,token 作为 Map 结构的 key,Module 实例作为值。
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
); // 生成token
if (this.modules.has(token)) {
return this.modules.get(token);
}
const moduleRef = new Module(type, this); // 实例化内建Module类
moduleRef.token = token;
this.modules.set(token, moduleRef); // 添加在modules上
await this.addDynamicMetadata(
token,
dynamicMetadata,
[].concat(scope, type),
);
if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
scanModulesForDependencies
方法会找到容器中每个 module 上的一些元数据,把对应的元数据分别添加到刚才添加到容器中的 module 上面,这些元数据就是根据上面提到的 Module 装饰器的参数生成的;instanceLoader.createInstancesOfDependencies()
private async createInstances(modules: Map<string, Module>) {
await Promise.all(
[...modules.values()].map(async moduleRef => {
await this.createInstancesOfProviders(moduleRef);
await this.createInstancesOfInjectables(moduleRef);
await this.createInstancesOfControllers(moduleRef);
const { name } = moduleRef.metatype;
this.isModuleWhitelisted(name) &&
this.logger.log(MODULE_INIT_MESSAGE`${name}`);
}),
);
}
遍历 modules 然后生成 provider、Injectable、controller 的实例。生成实例的顺序上也是有讲究的,controller 是最后生成的。在生成实例的过程中,nest 还会先去找到构造器中的依赖项:
const dependencies = isNil(inject)
? this.reflectConstructorParams(wrapper.metatype as Type<any>)
: inject;
reflectConstructorParams<T>(type: Type<T>): any[] {
const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || [];
const selfParams = this.reflectSelfParams<T>(type);
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
return paramtypes;
}
PARAMTYPES_METADATA
就是 ts 中内置的;metadataKey design:paramtypes
,获取到构造参数类型信息;然后就可以先实例化依赖项;async instantiateClass(instances, wrapper, targetMetatype, contextId = constants_2.STATIC_CONTEXT, inquirer) {
const { metatype, inject } = wrapper;
const inquirerId = this.getInquirerId(inquirer);
const instanceHost = targetMetatype.getInstanceByContextId(contextId, inquirerId);
const isInContext = wrapper.isStatic(contextId, inquirer) ||
wrapper.isInRequestScope(contextId, inquirer) ||
wrapper.isLazyTransient(contextId, inquirer) ||
wrapper.isExplicitlyRequested(contextId, inquirer);
if (shared_utils_1.isNil(inject) && isInContext) {
instanceHost.instance = wrapper.forwardRef
? Object.assign(instanceHost.instance, new metatype(...instances))
: new metatype(...instances);
}
else if (isInContext) {
const factoryReturnValue = targetMetatype.metatype(...instances);
instanceHost.instance = await factoryReturnValue;
}
instanceHost.isResolved = true;
return instanceHost.instance;
}
instantiateClass
方法,依赖项作为第一个参数 instances 传入。这里的 new metatype(...instances)
把依赖项的实例作为参数全部传入。NestFactory.create
方法的执行逻辑大概如下