[NestJS 带你飞!] DAY15 - Dynamic Module

前面有介绍过 Module 的一些基本使用方式,然而有一项非常强大的功能没有被提及,就是 动态模组(Dynamic Module),它可以用很简单的方式去客制化 Provider 的内容,使该 Module 的 Provider 动态化,什麽意思呢?简单来说,就是我们希望这个 Module 是可以透过外部传入参数去设置 Provider 的内容,与一般 静态模组(Static Module) 不同的地方在於,静态模组建立後 Provider 即建立完毕,若要更改 Provider 相关配置则要变动这个 Module 内部的程序码;动态模组则是将可能会变动的部分 参数化,让使用者在使用此 Module 时,可以透过其提供的 静态方法 来带入参数,让 Provider 接受该参数并建立 Module。

https://ithelp.ithome.com.tw/upload/images/20210512/20119338XnHqmcLaeS.png

用生活中的例子来说明的话,静态模组就像一个专用遥控器,在没有去改写内部的规则之前,它只能针对特定设备做控制;动态模组就像一个万用遥控器,同样是控制设备,但只需要根据特定的操作就能去控制不同的设备。

设计 Dynamic Module

动态模组是很常使用的功能,其中,最常遇到的情境就是环境变数管理,设计一个 Module 专门处理环境变数,这样的情境非常适合使用动态模组来处理,原因是管理环境变数的逻辑通常是不变的,会变的部分仅仅是读取环境变数的档案路径等,透过动态模组的机制成功将其抽离成共用元件,降低耦合度。

注意:关於环境变数的介绍会在下篇做更详细的说明。

这篇我们会运用动态模组与 dotenv 来实作一套简单的环境变数管理模组,名称定为 ConfigurationModule

注意dotenv 是一套用於管理环境变数的套件,详细内容可以参考官方文件

目标是让 ConfigurationModule 提供一个静态方法 forRoot,它可以接受一个包含 key 值为 path 的物件参数,path.env 档的相对路径,透过 forRoot 将参数带给 ConfigurationService 来处理 .env 的档案并管理解析出来的变数。首先,透过 npm 安装 dotenv

$ npm install dotenv --save

透过 CLI 产生 ConfigurationModuleConfigurationService

$ nest generate module common/configuration
$ nest generate service common/configuration

接着打开 configuration.module.ts,替 ConfigurationModule 添加一个 forRoot 静态方法,回传的值即为 DynamicModule,而 DynamicModule 其实就是一个物件,与 @Module 装饰器内的参数大致相同,不同的是必须要带上 module 参数,其值为 ConfigurationModule 本身,另外,还有 global 参数可以使产生出来的 Module 变成全域:

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';

@Module({})
export class ConfigurationModule {

  static forRoot(): DynamicModule {
    return {
      providers: [
        ConfigurationService
      ],
      module: ConfigurationModule,
      global: true
    };
  }

}

注意:静态方法可以自行设计,但回传值必须为同步或非同步 DynamicModule,名称通常会使用 forRootregister

从上方程序码可以看出 @Module 的参数净空了,这是为什麽呢?因为我们只使用动态模组,所以没有特别设计静态模组的部分,但如果要设计也是可以的。

接下来要在 forRoot 设计包含 key 值为 path 的物件参数,并将 path 取出,运用 Value Provider 的方式将该值记录下来。先在 configuration 资料夹下新增 constants 资料夹,并在里面建立 token.const.ts 来管理 token

export const ENV_PATH = 'ENV_PATH';

调整 configuration.module.ts

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';
import { ENV_PATH } from './constants/token.const';

@Module({})
export class ConfigurationModule {

  static forRoot(options: { path: string }): DynamicModule {
    return {
      providers: [
        {
          provide: ENV_PATH,
          useValue: options.path
        },
        ConfigurationService
      ],
      exports: [
        ConfigurationService
      ],
      module: ConfigurationModule,
      global: true
    };
  }

}

最後就是设计 ConfigurationService 的内容了,在 constructor 注入刚才设计的环境变数路径 ENV_PATH,接着设计 setEnvironment 去读取并解析 .env 档,然後写入 config 属性中,最後设计一个 get(key: string) 的方法来提取要用的环境变数:

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

import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';

import { ENV_PATH } from './constants/token.const';

@Injectable()
export class ConfigurationService {

  private config: any;

  constructor(
    @Inject(ENV_PATH) private readonly path: string
  ) {
    this.setEnvironment();
  }

  public get(key: string): string {
    return this.config[key];
  }

  private setEnvironment(): void {
    const filePath = path.resolve(__dirname, '../../', this.path);
    this.config = dotenv.parse(fs.readFileSync(filePath));
  }

}

使用 Dynamic Module

设计完 ConfigurationModule 以後,先在专案路径下新增 development.env 档,并设定里面的内容:

USERNAME=HAO

注意:是新增在专案路径下,与 package.json 同层级,非 src

接着,调整 app.module.ts 的内容:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigurationModule } from './common/configuration/configuration.module';

@Module({
  imports: [
    ConfigurationModule.forRoot({ 
      path: `../${process.env.NODE_ENV || 'development'}.env`
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

调整 app.controller.ts 的内容,在 constructor 注入 ConfigurationService,并改写 getHello 回传值:

import { Controller, Get } from '@nestjs/common';
import { ConfigurationService } from './common/configuration/configuration.service';

@Controller()
export class AppController {
  constructor(
    private readonly configService: ConfigurationService
  ) {
  }

  @Get()
  getHello() {
    return { username: this.configService.get('USERNAME') };
  }
}

透过浏览器查看 http://localhost:3000,会得到 USERNAME 的值:
https://ithelp.ithome.com.tw/upload/images/20210515/20119338UwcodEJlcJ.png

小结

Dynamic Module 是非常好用且实用的功能,经常运用在资料库、环境变数管理等功能,不过需要对 Nest 的依赖注入机制有一定程度的了解,在基础稳固之後学习上比较不会有问题。这里附上今天的懒人包:

  1. Dynamic Module 是运用静态方法回传一个 DynamicModule 型别的物件。
  2. 善用 Dynaic Module 来抽离共用元件。
  3. DynamicModule 必须包含 module 参数。
  4. 静态方法名称通常取为 forRootregister

<<:  Day15 NiFi - 与 RDB 对接设定

>>:  [前端暴龙机,Vue2.x 进化 Vue3 ] Day21. 『小专题◕ᴥ◕』 Vue 旅游小帮手(二)

LeetCode解题 Day04

834. Sum of Distances in Tree https://leetcode.com...

[Day 01] 你要的全能IDE,Visual Studio Code,它来了!

前言 大家好,我是刚从硕士班毕业不到一年的社会新鲜人,目前担任小小的AI工程师。 兴趣是资料分析和深...

Day23 测试写起乃 - Spork

spork 为加速测试用套件,透过启用 DRB server 载入环境让你在执行测试时只需要载入一次...

DAY30 - 完赛心得与下一步

第一次参加铁人赛,原本以为超前部署,开赛前两个星期就开始准备文章存档 本以为一定妥当的啦,没想到後面...

离职事项:我被裁员了,该准备哪些东西?

前言 最近这两年受到疫情的冲击,尤其是从今年五月中开始疫情第三级警戒,许多企业开始裁员,失业率创近期...