快速入门nest.js(6/10)--原理细节

简介: 依赖注入我们将依赖的实例委托给IOC容器,在这里,这个IOC容器就是NestJS运行时系统本身,NestJS在这里处理所有繁重的工作,而不是尝试自己实现依赖注入。本质上,当我们“ask”类构造函数中的依赖项时,NestJS处理并检索返回给我们的对象,以及它可能需要的任何依赖项等等。

依赖注入


我们将依赖的实例委托给IOC容器,在这里,这个IOC容器就是NestJS运行时系统本身,NestJS在这里处理所有繁重的工作,而不是尝试自己实现依赖注入。本质上,当我们“ask”类构造函数中的依赖项时,NestJS处理并检索返回给我们的对象,以及它可能需要的任何依赖项等等。


比如,当我们使用CoffeeService并将其注入到我们的构造函数中时,NestJS发生了什么才能使所有这些工作。

export class CoffeesController {
  constructor(private readonly coffeeService: CoffeesService){
  }
  // ...


在依赖注入过程中有3个关键步骤

  • 首先,在我们的CoffeeService中,@Injectable装饰器声明了一个可以由Nest容器管理的类,此装饰器将CoffeeService类标记为Provider
@Injectable()
export class CoffeesService {
    // ...
}


其次,如果我们进入CoffeesController,我们可以看到构造函数中正在请求CoffeesService,这个请求告诉Nest将提供程序inject到我们的控制器类中以便使用

export class CoffeesController {
  constructor(private readonly coffeeService: CoffeesService){
    // ...
  }
}


最后,Nest知道this类也是一个提供者,因为我们在CoffeeModule中包含了该提供者,,它向NestIOC容器注册了这个提供者,这就是它在我们代码本身的运作方式。

@Module({
// ...
  providers: [CoffeesService],
}


更加深入的理解:

当Nest容器实例化CoffeesController时,它首先查看是否由任何依赖项需要,在我们的例子中,有一个CoffeeService,当Nest容器找到CoffeesService依赖项时,它会对CoffeesService token执行查找,从而返回CoffeeService类,假设该Provider具有单例范围,这就是可注入提供程序的默认行为,然后,Nest将创建CoffeesService的实例,将其缓存并返回,或者已有缓存直接返回。<自下而上>



@Module({
// ...
  providers: [CoffeesService],
})

其实是下面的缩略写法:

@Module({
// ...
  providers: [{
      provide: CoffeesService,
      useClass: CoffeesService,
  }],
})


在这个完整的写法中,我们可以清楚地将TOKENCoffeesService与类CoffeesService相关联

控制module封装


默认情况下,NestJS模块封装了它们的提供者,这意味这无法注入不直接属于当前模块的提供者,也无法注入不是从导入模块导出的提供程序,因此你可以把从一个模块导出的提供者看作是该模块的公共接口。

// nest g mo coffee-rating
// nest g s coffee-rating
// 生成一个例子


假设我们的新CoffeeRatingService依赖CoffeesService从数据库中获取咖啡,而CoffeesService属于不同模块,所以我们需要在新的CoffeeRatingModule中导入CoffeesModule

使用基于构造函数的注入添加CoffeesService

// coffee-rating.service
import { Injectable } from '@nestjs/common';
import { CoffeesService } from 'src/coffees/coffees.service';
@Injectable()
export class CoffeeRatingService {
  constructor(private readonly coffeesService: CoffeesService){
    
  }
}


// coffee-rating.module
@Module({
  imports: [CoffeesModule],
  providers: [CoffeeRatingService]
})

此时如果直接运行会报依赖错误。

此时我们需要在CoffeesModule中将其添加到exports中:

// coffees.module
@Module({
// ...
  exports:[CoffeesService],
})


现在就没任何问题了...

深入CUSTOM PROVIDER



一些更为复杂的情况:

  1. 当我们正在创建我们的提供者的自定义实例,而不是让Nest为我们实例化该类;
  2. 假设我们想在第二个依赖项中重用现有类;
  3. 如果我们想用模拟版本覆盖一个类进行测试;
  4. 如果我们想使用策略模式,我们可以提供一个抽象类并根据不同的交换条件实际实现。

Nest允许我们自定义提供程序来处理这些用例

Value based Providers

useValue对于注入连续的值很有用,比如这里我们使用mock的数据MockCoffeesService

less

复制代码

class MockCoffeesService{}
@Module({
  imports:[TypeOrmModule.forFeature([Coffee, Flavor, Event])],
  controllers: [CoffeesController],
  providers: [{provide: CoffeesService, useValue: new MockCoffeesService}],  // 这里
  exports:[CoffeesService],
})


这时候,每当CoffeesService TOKEN被解析时,它将指向MockCoffeesService,任何时候偶们都会在我们的应用程序中注入CoffeesService

Nonclassbased Provider Tokens

有时我们可能希望灵活地使用字符串或符号作为依赖注入token


// coffees.module
@Module({
  imports:[TypeOrmModule.forFeature([Coffee, Flavor, Event])],
  controllers: [CoffeesController],
  providers: [CoffeesService, {provide: 'COFFEES_BRANDS', useValue: ['buddy brew', 'nescafe']}],
  exports:[CoffeesService],
})


那么现在我们该如何使用呢?

我们需要使用类名声明依赖项,因为这就是Nest在IOC容器中查找依赖项的方式。

// coffees.service
@Injectable()
export class CoffeesService {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository: Repository<Coffee>,
    @InjectRepository(Flavor)
    private readonly flavorRepository: Repository<Flavor>,
    private readonly connection: Connection,
    @Inject('COFFEES_BRANDS')  // 这里
    coffeeBrands: string[],
  ) {
    console.log(coffeeBrands);
  }


需要注意的是,虽然我们可以直接在内部使用COFFEES_BRANDS来注入装饰器,但最好在一个单独的文件定义Token,以便可以在整个应用程序中使用:

// /coffee创建coffees.constants.ts
export const COFFEES_BRANDS = "COFFEES_BRANDS";


然后将@Inject('COFFEES_BRANDS') // 这里换成对应的变量COFFEES_BRANDS就可以了<最佳实践>

Class Provider

useclass允许我们动态确定一个Token应该解析到的Class

// coffees.module
// ...
// 我们希望我们的config因环境而异
class ConfigService {}
class DevelopmentConfigService {}
class ProductionConfigService {}
@Module({
  imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
  controllers: [CoffeesController],
  providers: [
    CoffeesService,
    { provide: 'COFFEES_BRANDS', useValue: ['buddy brew', 'nescafe'] },
    {  // 这里
      provide: ConfigService,
      useClass:
        process.env.NODE_ENV === 'development'
          ? DevelopmentConfigService
          : ProductionConfigService,
    },
  ],
  exports: [CoffeesService],
})


Factor Provider

useFactory允许我们动态创建提供者,如果提供者的值是基于各种其他依赖项值等。因为它们本身可以注入计算返回结果所需的其他提供程序。

// coffees.module
@Injectable()
export class CoffeeBrandFactory{
  create(){
    /** do something */
    return ['buddy brew', 'nescafe']
  }
}
@Module({
  imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
  controllers: [CoffeesController],
  providers: [
    CoffeesService,
    CoffeeBrandFactory,
    { 
      provide: 'COFFEES_BRANDS', 
      useFactory: (brandFactory: CoffeeBrandFactory)=>brandFactory.create(),
      inject: [CoffeeBrandFactory],  // 这里会传递到useFactory的函数里,从而允许我们在其中随心所欲的使用
    },
// ...
  ],
  exports: [CoffeesService],
})


Leverage Async Provider

有时我们需要引导程序延迟执行,直到一个或多个Asynchronous Tasks完成,我们需要做的就是async/awaituseFactory相结合。

// coffees.module
  {
      provide: 'COFFEES_BRANDS',
      useFactory: async (connection: Connection): Promise<string[]> => {
        // const coffeeBrands = await connection.query('SLELECT * ...')
        const coffeeBrands = await Promise.resolve(['buddy brew', 'nescafe']);
        return coffeeBrands;
      },
      inject: [Connection],

动态模块



当我们有一个通用模块,该模块需要在不同情况下表现不同,将这个概念想象成一个博客系统,我们的动态模块需要一些配置才能被消费者使用。

这里出于演示,生成nest g mo database

import { Module } from '@nestjs/common';
import { createConnection } from 'typeorm';
@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: createConnection({
        type: 'postgres',
        host: 'localhost',
        port: 5432,
      }),
    },
  ],
})
export class DatabaseModule {}


由于我们对这些选项进行了硬编码,我们不能轻易地在不同地应用程序之间共享这个模块,如果另一个应用程序想要使用这个模块但它需要使用不同的端口怎么办?

这时候,我们就可以使用Nest的动态模块功能,我们可以让消费模块使用API来控制导入时自定义此DatabaseModule


import { DynamicModule, Module } from '@nestjs/common';
import { ConnectionOptions, createConnection } from 'typeorm';
@Module({
  imports:[  // 这里是导入的其他模块,只是这里演示简化了没导入
    DatabaseModule.register({
      type: 'postgres',
      host: 'localhost',
      password: 'pass123',
      port: 5432,
    })
  ]
}
)
export class DatabaseModule {
  static register(options: ConnectionOptions):DynamicModule{
    //DynamicModule与我们的@Modules具有基本相同的接口,但需要传递一个module属性
    return {
      module: DatabaseModule,
      providers:[
        {
          provide: 'CONNECTION',
          useValue: createConnection(options),
        }
      ]
    }
  }
}


控制Provider范围


NodeJS并不遵循请求/响应的多线程无状态模型,这种模型每个请求都由单独一个线程来处理。

因此,使用Singleton实例对我们的应用程序来说是完全安全的,但是在某些极端情况下,你可能需要提供者为某种所需行为提供基于请求的生命周期。

@INnjection()作用域允许我们获得所需的提供者生命周期行为,默认情况下,NestJS中的每个提供者都是一个单例。

@Injectable({scope: Scope.TRANSIENT})
export class CoffeesService {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository: Repository<Coffee>,
    @InjectRepository(Flavor)
    private readonly flavorRepository: Repository<Flavor>,
    private readonly connection: Connection,
    @Inject(COFFEES_BRANDS)
    coffeeBrands: string[],
  ) {
    console.log("CoffeesService intantiated");
  }


注意,该Service使用了两次,分别是在CoffeesController以及CoffeeBrandsFactory中,所以这里也实例化了两次。

如果我们删除{scope: Scope.TRANSIENT}会只实例化一次,该实例在CoffeesController以及CoffeeBrandsFactory中共享,这样会节约性能。

你也可以在Module里面像这样添加:

@Module({
// ...
  providers: [
    CoffeesService,
    {
      provide: 'COFFEES_BRANDS',
      useFactory: ()=>['buddy brew', 'nescafe'],
      scope:Scope.TRANSIENT,  // COFFEES_BRANDS就变成瞬态的了
      inject: [Connection],
    },


request-scoped

request-scoped会为每个到来的请求提供一个新的提供者实例,当然,在请求完成处理后,也会对实例进行垃圾收集

@Injectable({scope: Scope.REQUEST})


意味着该服务是为每个请求创建的。

CoffeesController是一个单例,为什么CoffeesService会被创建3次,我们并没有在控制器装饰器中修改任何东西?

其实在Nest中,这些scope会向上的注入链冒泡,这意味着如果CoffeesController依赖于属于REQUSET范围的CoffeesService,它也会隐式地变成REQUSET范围

我们在其中添加log:

typescript

复制代码

exportclassCoffeesController {
  // readonly是一种最佳实践  constructor(privatereadonly coffeeService: CoffeesService){    console.log("CoffeesController created");  }

然后请求三次:

这对于你访问请求特定信息,非常有用,例如标头、cookie、IP等

显示的使用:

export class CoffeesController {
  constructor(
    private readonly coffeeService: CoffeesService,
    @Inject(REQUEST) private readonly request: Request,
  ) {
    console.log('CoffeesController created');
  }
    //...
目录
相关文章
|
21天前
|
JavaScript 算法 前端开发
JS垃圾回收机制的原理
JS垃圾回收机制的原理
|
21天前
|
JavaScript 前端开发
解释JavaScript闭包的工作原理,并举例说明其在游戏开发中的应用。
JavaScript闭包允许内部函数访问并保持对外部函数变量的引用,即使外部函数执行结束。当函数返回内部函数时,形成闭包,继承父函数作用域链。在游戏开发中,闭包用于创建具有独立状态和行为的角色实例。例如,`createCharacter`函数创建角色并返回包含属性和方法的对象,内部函数如`getHealth`、`setHealth`和`attack`通过闭包访问并操作角色的变量。这种方式确保了每个角色的状态在不同实例间独立,是实现游戏逻辑的强大工具。
17 2
|
14天前
|
存储 监控 JavaScript
Node.js 性能平台5分钟快速入门
首先,确保拥有阿里云账号并开通服务,以及一台可上网的服务器。然后,创建应用并记下App ID和App Secret。通过tnvm安装Node.js性能平台组件,包括alinode和agenthub,检查安装成功的方法是`which node`和`which agenthub`命令显示路径包含`.tnvm`。接着,启动agenthub,并在服务器上运行一个示例应用(demo.js),该应用模拟计算密集型任务。最后,通过阿里云控制台观察监控数据和执行诊断操作。注意,性能平台每分钟上传一次日志,可能需等待几分钟才能看到数据。详细部署指南可参考官方文档。
28 6
|
20天前
|
Web App开发 JavaScript 前端开发
浏览器与Node.js事件循环:异同点及工作原理
浏览器与Node.js事件循环:异同点及工作原理
|
21天前
|
JavaScript
JS数组增删方法的原理,使用原型定义
JS数组增删方法的原理,使用原型定义
|
1天前
|
JavaScript 前端开发 Serverless
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
17 2
|
1天前
|
JavaScript 前端开发 Java
JavaScript快速入门
JavaScript快速入门
6 1
|
9天前
|
JavaScript 前端开发 Java
JavaScript 快速入门手册
JavaScript 快速入门手册
|
20天前
|
JavaScript 前端开发
探索JavaScript中的New操作符:原理与使用
探索JavaScript中的New操作符:原理与使用
|
21天前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。