快速入门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');
  }
    //...
目录
相关文章
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
1月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
36 1
|
1月前
|
数据采集 JavaScript 前端开发
JavaScript逆向爬虫——无限debugger的原理与绕过
JavaScript逆向爬虫——无限debugger的原理与绕过
|
5月前
|
Web App开发 前端开发 JavaScript
技术心得记录:瀑布流的布局原理分析(纯CSS瀑布流与JS瀑布流)
技术心得记录:瀑布流的布局原理分析(纯CSS瀑布流与JS瀑布流)
56 0
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript闭包:原理与应用
【10月更文挑战第11天】深入理解JavaScript闭包:原理与应用
19 0
|
1月前
|
JavaScript 前端开发 开发者
深入理解JavaScript中的闭包:原理与应用
【10月更文挑战第8天】深入理解JavaScript中的闭包:原理与应用
|
2月前
|
前端开发 JavaScript Java
JavaScript的运行原理
JavaScript 的运行原理包括代码输入、解析、编译、执行、内存管理和与浏览器交互几个步骤。当打开网页时,浏览器加载 HTML、CSS 和 JavaScript 文件,并通过 JavaScript 引擎将其解析为抽象语法树(AST)。接着,引擎将 AST 编译成字节码或机器码,并在执行阶段利用事件循环机制处理异步操作,确保单线程的 JavaScript 能够高效运行。同时,JavaScript 引擎还负责内存管理和垃圾回收,以减少内存泄漏。通过与 DOM 的交互,JavaScript 实现了动态网页效果,提供了灵活且高效的开发体验。
|
2月前
|
存储 JavaScript 前端开发
[JS] ES Modules的运作原理
【9月更文挑战第16天】ES Modules(ECMAScript Modules)是 JavaScript 中的一种模块化开发规范,适用于浏览器和 Node.js 环境。它通过 `export` 和 `import` 关键字实现模块的导出与导入。模块定义清晰,便于维护和测试。JavaScript 引擎会在执行前进行静态分析,确保模块按需加载,并处理循环依赖。ES Modules 支持静态类型检查,现代浏览器已原生支持,还提供动态导入功能,增强了代码的灵活性和性能。这一规范显著提升了代码的组织和管理效率。
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析