首先 nestjs 是什么?引用其官网的原话 A progressive Node.js framework for building efficient, reliable and scalable server-side applications. ,翻译一下就是:“一个可以用来搭建高效、可靠且可扩展的服务端应用的node框架”。目前在 github 上有 42.4k 的 star 数,人气还是很高的。
在使用过程中会发现 nest 框架和后端同学使用的 Springboot 以及前端三大框架之一的 angular 都有很多相似之处。没错这三个框架都有相似的设计,并都实现了依赖注入。
可能对大部分前端同学来说, 依赖注入 这个词还比较陌生,本文就围绕 依赖注入 这个话题,展开讨论一下依赖注入是什么?以及在 nestjs 中详细的实现过程。
先来看看几个重要概念的解释
依赖倒置原则( DIP ):抽象不应该依赖实现,实现也不应该依赖实现,实现应该依赖抽象。
依赖注入(dependency injection,简写为 DI):依赖是指依靠某种东西来获得支持。将创建对象的任务转移给其他class,并直接使用依赖项的过程,被称为“依赖项注入”。
控制反转(Inversion of Control, 简写为 IoC):指一个类不应静态配置其依赖项,应由其他一些类从外部进行配置。
光看上面的解释可能并不好理解?那么我们把概念和具体的代码结合起来看。
根据 nest 官网教程,用脚手架创建一个项目,创建好的项目中有 main.ts 文件为入口文件,引入了 app.module.ts 文件,而 app.module.ts 文件引入了 app.controller.ts。先看一下代码的逻辑:
// 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 实例化时的参数传给构造器就是 依赖注入 了。
依赖注入的实现主要有三种方式
构造器注入: 依赖关系通过 class 构造器提供;
setter 注入:用 setter 方法注入依赖项;
接口注入:依赖项提供一个注入方法,该方法将把依赖项注入到传递给它的任何客户端中。客户端必须实现一个接口,该接口的 setter 方法接收依赖; 在 nest 中采用了第一种方式——构造器注入。
那么 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 容器作为参数传入,这个类是专门用来生成需要注入的实例对象的;
实例化 MetadataScanner 类和 DependenciesScanner 类,MetadataScanner 类是一个用来获取 元数据 的工具类,而 DependenciesScanner 类是用来扫描出所有 modules 中的依赖项的。上面的 app.module.ts 中 Module 装饰器的参数中传入了 controllers 、 providers 等其他选项,这个 Module 装饰器的作用就是标明 AppModule 类的一些依赖项;
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
调用依赖扫描器的 scan 方法,扫描依赖;
public async scan(module: Type<any>) {
// 1. 把一些内建module添加到IoC容器中
await this.registerCoreModule();
// 2. 把传入的module添加到IoC容器中
await this.scanForModules(module);
// 3. 扫描当前IoC容器中所有module的依赖
await this.scanModulesForDependencies();
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);
}
// 生成token
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);
if (this.modules.has(token)) {
return this.modules.get(token);
}
// 实例化内建Module类
const moduleRef = new Module(type, this);
moduleRef.token = token;
// 添加在modules上
this.modules.set(token, moduleRef);
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 方法的执行逻辑大概如下
元数据反射是实现依赖注入的基础;
总结依赖注入的过程,nest 主要做了三件事情
原文:https://www.zoo.team/article/nestjs
canvas-nest.js是一款轻量的网页特效。在vue项目中,使用时配置,引入canvas-nest.js后,直接在created中 new CanvasNest(element, config)。遇到parameter 1 is not of type Element这样类型的报错,需要检查dom是否渲染完毕。
最近已经使用过一段时间的nestjs,让人写着有一种java spring的感觉,nestjs可以使用express的所有中间件,此外完美的支持typescript,与数据库关系映射typeorm配合使用可以快速的编写一个接口网关。本文会介绍一下作为一款企业级的node框架的特点和优点。
最近在学习研究 Nest 框架,但是在学习过程中除了参考翻阅官方文档外国内几乎没有多少资料能系统的讲解 Nest 的相关内容,所以打算想通过我自己学习的角度讲解下 Nest 框架,不知道能坚持多久
最近上了一个新项目,这个客户管理一个庞大的任务和团队集群,而不同流程所适用的系统也不太一样,比如salesforce,hubspots之类的。这次的新项目需要在另外两个平台之间做一些事情
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!