[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 可以产生识别码与从请求物件中取得识别码。

进阶功能的部分到这边告一段落,下一篇开始将会进入到 多元化功能 单元,敬请期待!


<<:  [13th][Day19] http request header(上)

>>:  Day20-React 简易动画篇-上篇

[Day1] 让开发者森77之前

前言 这个系列主要会介绍一些Web Application攻击手法和一些Real World的案例以...

[day-7] 在正式开始写程序之前,先来认识电脑本身吧!(Part .2)

前情提要 昨天 [day-6] 大致介绍了,电脑的起源与相关发展史,相信各位读资讯或是商业领域的人应...

Day12.进入 ARM 世界: ARM Cortex-M Exception Behavior

Nested Interrupts Cortex-M3 和 NVIC 在硬体架构上支援(Nested...

如何制作一个精美的网站

什麽是好的网站设计? 使用者使用网站时是否容易操作及有良好的动线,避免过多不必要的元素,让使用者快速...

Day15 - this&Object Prototypes Ch3 Objects - Iteration 开头

搭配 in 的 for 回圈会搜寻物件中所有 property 为 enumerable 者 而使...