iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Modern Web

NestJS 帶你飛!系列 第 19

[NestJS 帶你飛!] DAY19 - Module Reference

前面有提過,注入 Provider 的方式只需要在 constructor 設計參數並附上對應的型別,或使用 @Inject 裝飾器來取得對應的實例,以 app.controller.ts 為例,在 constructor 中直接填入參數並附上型別為 AppService 就可以取得 AppService 的實例:

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();
  }
}

事實上,Nest 還有提供另一種不同的方式來取得內部 Provider 的實例,它叫 模組參照 (Module Reference)

什麼是 Module Reference?

它是一個名叫 ModuleRefclass,可以對內部 Provider 做一些存取,可以說是該 Module 的 Provider 管理器。

使用 Module Reference

使用上與 Provider 注入的方式相同,在 constructor 注入即可,以 app.controller.ts 為例:

import { Controller } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

@Controller()
export class AppController {
  constructor(
    private readonly moduleRef: ModuleRef
    ) {}
}

獲取實例

注入 ModuleRef 以後,可以透過 get 方法來取得 當前 Module 下的 任何元件,如:Controller、Service、Guard 等。

注意:此方法無法在非預設作用域的配置下使用。

這裡以 app.controller.ts 為例,我們透過 ModuleRef 來取得 AppService 的實例:

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

@Controller()
export class AppController {

  private readonly appService: AppService;

  constructor(
    private readonly moduleRef: ModuleRef
    ) {
      this.appService = this.moduleRef.get(AppService);
    }

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

透過瀏覽器查看 http://localhost:3000 是能夠正常存取的,表示該方法是正常運作的:
https://ithelp.ithome.com.tw/upload/images/20210527/20119338HHPGJQWCOl.png

獲取全域實例

如果要取得 全域 的實例,需要給定參數 strictfalse 即可取得全域範圍的實例,這裡先產生一個 StorageModuleStorageService,並將該 Module 設置為全域:

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

修改 storage.module.ts 的內容:

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

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

修改 storage.service.ts 的內容:

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

@Injectable()
export class StorageService {

  private list: any[] = [];

  public addData(data: any): void {
    this.list.push(data);
  }

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

}

接著,調整 app.controller.ts 的內容,透過 ModuleRef 來取得全域實例 - StorageService

import { Controller, Get } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { StorageService } from './common/storage/storage.service';

@Controller()
export class AppController {

  private readonly storageService: StorageService;

  constructor(
    private readonly moduleRef: ModuleRef
    ) {
      this.storageService = this.moduleRef.get(StorageService, { strict: false });
      this.storageService.addData({ name: 'HAO' });
    }

    @Get()
    getHello() {
      return this.storageService.getList();
    }
}

透過瀏覽器查看 http://localhost:3000 能夠正常存取,表示該方法是正常運作的:
https://ithelp.ithome.com.tw/upload/images/20210527/20119338xXPwp6vmMx.png

處理非預設作用域之 Provider

既然在非預設作用域的配置下無法使用 get 來取得實例,那該如何處理呢?這時候可以透過 resolve 來解決,resolve 會從自身的 依賴注入容器子樹 (DI container sub-tree) 返回實例,而每個子樹都有一個獨一無二的 識別碼 (Context Identifier),因此每次 resolve 都會是 不同的實例

來做個簡單的實驗,先將 AppService 轉化成請求作用域:

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

@Injectable({ scope: Scope.REQUEST })
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

接著,我們在 AppController 中使用兩次 resolve 並比對他們是否為相同的實例,啟動 Nest 之後,會在終端機看到比對結果為 false

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

@Controller()
export class AppController implements OnModuleInit {
  constructor(
    private readonly moduleRef: ModuleRef
  ) {}

  async onModuleInit() {
    const [instance1, instance2] = await Promise.all([
      this.moduleRef.resolve(AppService),
      this.moduleRef.resolve(AppService)
    ]);

    console.log(instance1 === instance2); // false
  }

}

手動配置識別碼

如果要將多個 resolve 回傳相同的實例,可以透過指定識別碼讓它們使用相同的子樹,進而取得相同的實例。在指定識別碼之前,可以透過 ContextIdFactory 這個 classcreate 方法來產生識別碼。這裡同樣以 AppController 為例,在 resolve 之前先產生識別碼並帶入 resolve 中,啟動 Nest 之後,會在終端機看到結果為 true

import { Controller, OnModuleInit } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { AppService } from './app.service';

@Controller()
export class AppController implements OnModuleInit {
  constructor(
    private readonly moduleRef: ModuleRef
  ) {}

  async onModuleInit() {
    const identifier = ContextIdFactory.create();
    const [instance1, instance2] = await Promise.all([
      this.moduleRef.resolve(AppService, identifier),
      this.moduleRef.resolve(AppService, identifier)
    ]);

    console.log(instance1 === instance2); // true
  }

}

共享子樹

透過手動產生的識別碼並不會配置到 Nest 的依賴注入系統中,因此,你可以透過 ModuleRefregisterRequestByContextId 方法將手動產生的識別碼與注入的請求物件做綁定,後續可以透過 ContextIdFactorygetByRequest 將識別碼從請求物件中取出,進而達到共享子樹的效果。

我們這裡做個實驗,在 AppService 建構時,就產生一個識別碼並綁定到注入的請求物件中:

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

import { Request } from 'express';

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

  constructor(
    @Inject(REQUEST) private readonly request: Request,
    private readonly moduleRef: ModuleRef
  ) {
    const identifier = ContextIdFactory.create();
    this.moduleRef.registerRequestByContextId(this.request, identifier);
  }

}

AppController 中也注入 REQUEST 並透過 getByRequest 取得識別碼,根據該識別碼來執行兩次 resolve 以及比對實例:

import { Controller, Get, Inject } from '@nestjs/common';
import { ContextIdFactory, ModuleRef, REQUEST } from '@nestjs/core';

import { Request } from 'express';

import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly moduleRef: ModuleRef,
    @Inject(REQUEST) private readonly request: Request
  ) {}

  @Get()
  async getTruth() {
    const identifier = ContextIdFactory.getByRequest(this.request);
    const [instance1, instance2] = await Promise.all([
      this.moduleRef.resolve(AppService, identifier),
      this.moduleRef.resolve(AppService, identifier)
    ]);

    return instance1 === instance2;
  }

}

透過瀏覽器查看 http//:localhost:3000 會得到結果為 true

小結

這裡附上今天的懶人包:

  1. ModuleRef 可以對內部 Provider 做一些存取。
  2. ModuleRef 預設只能取得當前模組下的實例,透過調整 strict 才能取得全域實例。
  3. 非預設作用域要使用 resolve 來取得實例。
  4. 預設情況下,每次 resolve 回傳的實例都不同,需要透過指定識別碼來配置成相同實例。
  5. 可以用 ModuleRef 來綁定識別碼。
  6. 透過 ContextIdFactory 可以產生識別碼與從請求物件中取得識別碼。

進階功能的部分到這邊告一段落,下一篇開始將會進入到 多元化功能 單元,敬請期待!


上一篇
[NestJS 帶你飛!] DAY18 - Lifecycle Hooks
下一篇
[NestJS 帶你飛!] DAY20 - File Upload
系列文
NestJS 帶你飛!32

尚未有邦友留言

立即登入留言