本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
Nest 在大多數情況下是採用 單例模式 (Singleton pattern) 來維護各個實例,也就是說,各個進來的請求都共享相同的實例,這些實例會維持到 Nest App 結束為止。但有些情況可能就需要針對各個請求做處理,這時候可以透過調整 注入作用域 (Injection scope) 來決定實例的建立時機。
注意:雖然說可以調整建立實例時機,但如果非必要還是建議採用單例模式,原因是可以提升系統整體效能,若針對每個請求建立實例,將會花費更多資源在處理建立與垃圾回收。
Nest 共有三種作用域可以使用:
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
裝飾器的參數即可,同樣使用選項參數來配置,若有路由設定,將其配置在 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();
}
}
作用域的配置會影響整個注入鏈作用域範圍,什麼意思呢?這裡用下方圖示作為範例:
可以看到 StorageService
分別在 AppModule
與 BookModule
被使用,而 BookService
又在 AppModule
被使用,此時,如果我們把 StorageService
的作用域設置為「請求作用域」,那麼依賴於 StorageService
的 BookService
與 AppService
都會變成請求作用域,所以按這樣的邏輯來看,AppController
也會變成請求作用域,因為它依賴了 AppService
:
但如果是把 BookService
設為「請求作用域」,那就僅有 AppService
與 AppController
會是請求作用域,因為 StorageService
不依賴於 BookService
:
由於請求作用域是針對每一個請求來建立實例,所以能透過注入 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!';
}
}
這裡來做個簡單的實驗,來驗證各個作用域的實例化時間與實例的共享,會使用上面 AppModule
、BookModule
與 StorageModule
的架構。首先,先來建立 StorageModule
與 BookModule
:
$ 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.ts
與 app.controller.ts
了,這裡我們先改一下 app.service.ts
的內容,將 BookService
與 StorageService
注入,並為它們各設計一套存取方法,然後也在 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
去呼叫 BookService
與 StorageService
的存入方法,並設計一個 /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,會發現與我們預期是相同的,BookModule
與 AppModule
會共用同一個 StorageService
,所以才會回傳兩個一模一樣的資料:
我們將請求作用域配置在 BookService
上,所以理論上 StorageService
會是單例的,而 BookService
、AppService
與 AppController
會是請求作用域。這裡修改一下 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
:
這部分我們改成將 StorageService
設置成獨立作用域,所以將 BookService
的 scope
移除,並修改 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 之間是不共享實例的,而 StorageService
在 BookService
與 AppService
各建立了一次,所以會有兩個實例。
透過瀏覽器查看 http://localhost:3000/compare,會發現兩者資料不一致,這與我們預期是相同的,因為它們是不同的實例:
在一般情況下其實不太會去變動注入作用域的範圍,但在某些特定情況下是必要的,雖然說不太會變動,但我認為這篇的內容可以對 Nest 的依賴注入規則有更進一步的理解。這邊附上今天的懶人包:
REQUEST
來取得請求物件。