[NestJS 带你飞!] DAY17 - Injection Scopes

Nest 在大多数情况下是采用 单例模式 (Singleton pattern) 来维护各个实例,也就是说,各个进来的请求都共享相同的实例,这些实例会维持到 Nest App 结束为止。但有些情况可能就需要针对各个请求做处理,这时候可以透过调整 注入作用域 (Injection scope) 来决定实例的建立时机。

注意:虽然说可以调整建立实例时机,但如果非必要还是建议采用单例模式,原因是可以提升系统整体效能,若针对每个请求建立实例,将会花费更多资源在处理建立与垃圾回收。

作用域

Nest 共有三种作用域可以使用:

  1. 预设作用域 (Default scope):即单例模式之作用域。
  2. 请求作用域 (Request scope):为每个请求建立全新的实例,在该请求中的 Provider 是共享实例的,请求结束後将会进行垃圾回收。
  3. 独立作用域 (Transient scope):每个 Provider 都是独立的实例,在各 Provider 之间不共享。

Provider 设置作用域

Provider 设定作用域只要在 @Injectable 装饰器中做配置即可,它有提供一个选项参数,透过填入 scope 来做指定,而作用域参数可以透过 Nest 提供的 enum - Scope 来配置。以 app.service.ts 为例:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class AppService {

  getHello(): string {
    return 'Hello World!';
  }

}

如果是自订 Provider 的话,就多一个 scope 的属性。以 app.module.ts 为例:

import { Module, Scope } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService,
    {
      provide: 'USERNAME',
      useValue: 'HAO',
      scope: Scope.REQUEST // 添加 scope 属性
    }
  ]
})
export class AppModule {
}

Controller 设置作用域

Controller 设定作用域只要调整 @Controller 装饰器的参数即可,同样使用选项参数来配置,若有路由设定,将其配置在 path 属性,而作用域则是 scope。以 app.controller.ts 为例:

import { Controller, Get, Scope } from '@nestjs/common';
import { AppService } from './app.service';

@Controller({ scope: Scope.REQUEST })
export class AppController {
  constructor(
    private readonly appService: AppService
  ) {
  }

  @Get()
  getHello() {
    return this.appService.getHello();
  }

}

作用域冒泡

作用域的配置会影响整个注入链作用域范围,什麽意思呢?这里用下方图示作为范例:
https://ithelp.ithome.com.tw/upload/images/20210523/201193381IQZTZrN60.png

可以看到 StorageService 分别在 AppModuleBookModule 被使用,而 BookService 又在 AppModule 被使用,此时,如果我们把 StorageService 的作用域设置为「请求作用域」,那麽依赖於 StorageServiceBookServiceAppService 都会变成请求作用域,所以按这样的逻辑来看,AppController 也会变成请求作用域,因为它依赖了 AppService
https://ithelp.ithome.com.tw/upload/images/20210523/201193380Ba7pCPiPN.png

但如果是把 BookService 设为「请求作用域」,那就仅有 AppServiceAppController 会是请求作用域,因为 StorageService 不依赖於 BookService
https://ithelp.ithome.com.tw/upload/images/20210523/201193387hraBJGFfQ.png

请求作用域与请求物件

由於请求作用域是针对每一个请求来建立实例,所以能透过注入 REQUEST 来取得请求物件。以 app.service.ts 为例:

import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';

import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class AppService {

  constructor(
    @Inject(REQUEST) private readonly request: Request
  ) {}

  getHello(): string {
    return 'Hello World!';
  }
}

实例化实验

这里来做个简单的实验,来验证各个作用域的实例化时间与实例的共享,会使用上面 AppModuleBookModuleStorageModule 的架构。首先,先来建立 StorageModuleBookModule

$ nest generate module common/storage
$ nest generate service common/storage
$ nest generate module common/book
$ nest generate service common/book

接着,设计一下 storage.service.ts 的内容,在 contructor 印出含有乱数的字串,透过乱数可以让我们清楚知道该实例是否为同一个实例,也可以运用这样的方式观察建立实例的时机,然後设计一套添加资料与取得资料的方法:

import { Injectable } from '@nestjs/common';

@Injectable()
export class StorageService {

  constructor() {
    console.log(`Storage: ${Math.random()}`);
  }

  private list: any[] = [];

  public getItems(): any[] {
    return this.list;
  }

  public addItem(item: any): void {
    this.list.push(item);
  }

}

调整 storage.module.ts 的内容,将 StorageService 汇出:

import { Module } from '@nestjs/common';
import { StorageService } from './storage.service';

@Module({
  providers: [
    StorageService
  ],
  exports: [
    StorageService
  ]
})
export class StorageModule {}

再来我们设计一下 book.service.ts 的内容,将 StorageService 注入并设计一套存取资料的方法,同样在 constructor 印出含有乱数的字串:

import { Injectable } from '@nestjs/common';
import { StorageService } from '../storage/storage.service';

@Injectable()
export class BookService {

  constructor(
    private readonly storage: StorageService
  ) {
    console.log(`Book: ${Math.random()}`);
  }

  public getBooks(): any[] {
    return this.storage.getItems();
  }

  public addBook(book: any): void {
    this.storage.addItem(book);
  }

}

因为有用到 StorageService,故要引入 StorageModule,这边我们修改一下 book.module.ts,并将 BookService 汇出:

import { Module } from '@nestjs/common';
import { StorageModule } from '../storage/storage.module';
import { BookService } from './book.service';

@Module({
  imports: [
    StorageModule
  ],
  providers: [
    BookService
  ],
  exports: [
    BookService
  ]
})
export class BookModule {}

最後就是调整 app.service.tsapp.controller.ts 了,这里我们先改一下 app.service.ts 的内容,将 BookServiceStorageService 注入,并为它们各设计一套存取方法,然後也在 constructor 印出含有乱数之字串:

import { Injectable } from '@nestjs/common';
import { BookService } from './common/book/book.service';
import { StorageService } from './common/storage/storage.service';

@Injectable()
export class AppService {

  constructor(
    private readonly bookService: BookService,
    private readonly storage: StorageService
  ) {
    console.log(`AppService: ${Math.random()}`);
  }

  public addBookToStorage(book: any): void {
    this.storage.addItem(book);
  }

  public addBookToBookStorage(book: any): void {
    this.bookService.addBook(book);
  }

  public getStorageList(): any[] {
    return this.storage.getItems();
  }

  public getBookList(): any[] {
    return this.bookService.getBooks();
  }

}

修改 app.controller.ts,在 constructor 透过 AppService 去呼叫 BookServiceStorageService 的存入方法,并设计一个 /compare 的路由来看看是否存取相同的 StorageService 实例:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService
  ) {
    this.appService.addBookToStorage({ name: 'Nest Tutorial' });
    this.appService.addBookToBookStorage({ name: 'Angular Tutorial' });
    console.log(`AppController: ${Math.random()}`);
  }

  @Get('/compare')
  getCompare() {
    return {
      storage: this.appService.getStorageList(),
      books: this.appService.getBookList()
    };
  }

}

预设作用域

预设作用域不需要特别指定,所以我们直接启动 Nest App 即可,启动後会在终端机看到下方的讯息:

Storage: 0.5154167235100049
Book: 0.003178436868019663
AppService: 0.19088741578100654
AppController: 0.70972377329212

这代表什麽呢?因为是单例模式,在 Nest 建立的时候所有的依赖项目都会被建立起来,并持续到 Nest 关闭为止,所以我们才会在启动时就看见这些字串,并且不会再看见它们,直到下次重新启动。

透过浏览器查看 http://localhost:3000/compare,会发现与我们预期是相同的,BookModuleAppModule 会共用同一个 StorageService,所以才会回传两个一模一样的资料:
https://ithelp.ithome.com.tw/upload/images/20210523/20119338jAIDuW7mYX.png

请求作用域

我们将请求作用域配置在 BookService 上,所以理论上 StorageService 会是单例的,而 BookServiceAppServiceAppController 会是请求作用域。这里修改一下 book.service.ts

import { Injectable, Scope } from '@nestjs/common';
import { StorageService } from '../storage/storage.service';

@Injectable({ scope: Scope.REQUEST })
export class BookService {

  constructor(
    private readonly storage: StorageService
  ) {
    console.log(`Book: ${Math.random()}`);
  }

  public getBooks(): any[] {
    return this.storage.getItems();
  }

  public addBook(book: any): void {
    this.storage.addItem(book);
  }

}

接着重新启动 Nest App,会在终端机看到下方讯息:

Storage: 0.68586411156073

为什麽只有看到 StorageService 印出来的资讯呢?原因是 StorageService 保持在单例模式,所以在启动时就会被建立,但 BookService 是请求作用域,当有请求进来的时候才会被实例化,所以才会没有显示出来。

透过浏览器查看 http://localhost:3000/compare,会发现与预期结果相同,在终端机上会看到下方讯息:

Book: 0.0333570635121212
AppService: 0.6894665444881014
AppController: 0.47336587362981764

然後浏览器上显示的结果与预设作用域相同,不过如果这时候按下重新整理的话,会发现资料变多了,原因是我们只要在 AppController 实例化的时候就添加资讯,所以才会增加资料到 StorageService
https://ithelp.ithome.com.tw/upload/images/20210523/20119338GBZG2rNXf7.png

独立作用域

这部分我们改成将 StorageService 设置成独立作用域,所以将 BookServicescope 移除,并修改 storage.service.ts

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class StorageService {

  constructor() {
    console.log(`Storage: ${Math.random()}`);
  }

  private list: any[] = [];

  public getItems(): any[] {
    return this.list;
  }

  public addItem(item: any): void {
    this.list.push(item);
  }

}

重新启动 Nest App,会在终端机看到以下讯息:

Storage: 0.15469395107871975
Storage: 0.8083162424289829
Book: 0.7182461464132914
AppService: 0.9978782563846749
AppController: 0.5198170904633788

会发现 StorageService 建立了两个实例,原因是独立作用域在各个 Provider 之间是不共享实例的,而 StorageServiceBookServiceAppService 各建立了一次,所以会有两个实例。

透过浏览器查看 http://localhost:3000/compare,会发现两者资料不一致,这与我们预期是相同的,因为它们是不同的实例:
https://ithelp.ithome.com.tw/upload/images/20210523/201193385T9c23eiPl.png

小结

在一般情况下其实不太会去变动注入作用域的范围,但在某些特定情况下是必要的,虽然说不太会变动,但我认为这篇的内容可以对 Nest 的依赖注入规则有更进一步的理解。这边附上今天的懒人包:

  1. Nest 预设采用单例模式维护实例。
  2. 透过改变注入作用域的范围来改变实例的维护规则。
  3. 共有三个作用域规则:预设作用域、请求作用域、独立作用域。
  4. 预设作用域即单例模式。
  5. 请求作用域会针对各个请求建立实例。
  6. 独立作用域会使各 Provider 之间不共享。
  7. 请求作用域可以透过注入 REQUEST 来取得请求物件。

<<:  Powershell 入门之 policy

>>:  [Day 24] BDD - godog 小试身手

离职倒数14天:2020年学到最多的两件事,解答了我人生最大的困惑

今年年初回顾2020年时的日记里写着,去年学到最多的两件事:一个是趁着肺炎,工作後第一次回家长住,一...

【在 iOS 开发路上的大小事-Day27】透过 Firebase 来管理资料 (Cloud Firestore 篇) Part1

前置作业 在 Podfile 里面新增 Firebase Realtime Database 套件 ...

你要的是Entity Framework吗?

很多初学Entity Framework( Core)(以下简称EF)的新手,刚开始使用EF时都会有...

Day1 工业控制系统与普渡模型

工业控制系统 Industrial Control System 简称 ICS = 电脑与工业设备...

LINE BOT聊天机器人-查询天气资讯

遮是一篇超级没有语言技术性质的文章!请三思慎入!! 今天要来做查询天气的功能。 一样有事前作业: 1...