[NestJS 带你飞!] DAY16 - Configuration

前一篇我们运用 Dynamic Module 与 dotenv 设计了一个简单的环境变数管理模组,但什麽是环境变数?又为什麽要做环境变数的管理?那有没有现成的轮子可以使用?接下来会一一告诉各位!

环境变数

一套系统通常会执行在各个不同环境上,最简单的区分为:开发环境与正式环境,会这样区别的原因是我们不希望在测试系统的时候去影响到正式环境的资料,所以会将资料库等配置分成两组,也就会有两组资料库的连接资讯需要被记录与使用,这时候要仔细想想该如何做好这些敏感资讯的配置又能快速切换环境,将资讯直接写在程序码里头绝对是不理想的方式,於是就有 环境变数(Environment Variable) 的概念。

环境变数与一般变数不同的地方在於,环境变数是透过程序码以外的地方做指定,这种变数可以直接在作业系统上设定,也可以透过指令的方式做设定,以 node.js 为例,可以直接在指令中做配置:

$ NODE_ENV=production node index.js

如此一来,便可以在 process.env 取得环境变数,但如果每次都要这样输入与调用实在很难管理,於是就有环境变数档的概念出现,在 node.js 最常用的就是 .env 档,其设计方式很简单,等号的左边为 key 值,右边为 value

USERNAME=HAO

在 Nest 中,可以使用官方制作的 ConfigModule 来读取并管理这些环境变数,当然,要自行设计也可以,透过 Dynamic Module 的概念来实作即可。

安装 ConfigModule

既然有造好的轮子可以使用,且前一篇也有简单的带过如何用 Dynamic Module 实作,故这篇就专门介绍官方实作的套件。该套件并不是内建的,需要额外安装,透过 npm 进行安装即可:

提醒:若前一篇有安装过 dotenv 可以先行移除。

$ npm install @nestjs/config --save

使用 ConfigModule

ConfigModule 也是使用 Dynamic Module 概念设计的,我们只需要在 AppModule 中调用其 forRoot 方法即可使用,以下方为例,修改 app.module.ts

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

@Module({
  imports: [
    ConfigModule.forRoot()
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

接着,在专案路径下新增 .env 档,并设置其内容:

USERNAME=HAO

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

修改 app.controller.ts,在 AppControllercontructor 注入 ConfigService,让 getHello 透过 ConfigServiceget 方法取出 USERNAME 并回传:

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

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

透过浏览器查看 http://localhost:3000
https://ithelp.ithome.com.tw/upload/images/20210516/201193389UCArVHvAh.png

使用自订环境变数档

预设状态下,ConfigModule 会从专案路径下取 .env 档来做为环境变数档,但我们常常会需要为不同环境配置多个档案,这时候就可以透过自订环境变数档来处理。ConfigModuleforRoot 静态方法有提供 envFilePath 参数来配置指定的 .env 档,以下方为例,我们去读取 development.env

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

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: 'development.env'
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

将刚才的 .env 档的名称变更为 development.env,并重新启动 Nest App,接着透过浏览器查看 http://localhost:3000
https://ithelp.ithome.com.tw/upload/images/20210516/20119338muHwuT5EW1.png

还有一种情况是本地测试使用的环境变数与其他环境下测试用的环境变数不相同,这时候可以使用优先权的方式做载入,假设本地端使用的环境变数档名为 development.local.env,而其他环境下使用的环境变数档名为 development.env,那就可以在 envFilePath 配置一个阵列,其内容为档案名称,越前面的优先权越高。这里我们先建立一个 development.local.env 并添加下方内容:

USERNAME=local_tester

修改一下 app.module.ts,让 development.local.env 的优先级别大於 development.env

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

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env']
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

透过浏览器查看 http://localhost:3000 会看到 usernamelocal_tester
https://ithelp.ithome.com.tw/upload/images/20210516/20119338q6dZCRg075.png

使用工厂函式

有些复杂的情境可以透过工厂模式来处理环境变数,比如:假设有配置 development.env,但有些比较不敏感的资讯可以直接使用预设值,故不需要在档案里面做相关配置,只需要在工厂函式里做配置即可。我们在 src 资料夹下创建一个名为 config 的资料夹,并在里面建立 configuration.factory.ts
https://ithelp.ithome.com.tw/upload/images/20210516/20119338IjH2kdsqYL.png

修改 configuration.factory.ts 的内容,让 PORT 采用预设值 3000

export default () => ({
  PORT: process.env.PORT || 3000
});

接着,修改 app.module.ts 的内容,添加 load 参数至 forRoot 静态方法中,其接受的型别为阵列,内容即工厂函式:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configurationFactory from './config/configuration.factory';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      load: [configurationFactory]
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

注意load 参数接受阵列是因为它可以使用多个工厂函式来处理环境变数。

修改 app.controller.ts,让 getHello 透过 ConfigServiceget 方法取出 USERNAMEPORT 并回传:

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

  @Get()
  getHello() {
    const username = this.configService.get('USERNAME');
    const port = this.configService.get('PORT');
    return { username, port };
  }
}

透过浏览器查看 http://localhost:3000
https://ithelp.ithome.com.tw/upload/images/20210516/20119338y6Yum7cgVT.png

使用工厂函式配置命名空间

由於环境变数在配置的时候是采用 = 来划分 keyvalue 的,并不能在 value 的地方延伸出下一个层级,所以环境变数层级是 扁平 的,没有办法按照类别做归类,以下方为例,假设环境变数档 development.local.env 里面有下方资讯:

DB_HOST=example.com
DB_PASSWORD=12345678
PORT=3000

可以很明显看出 DB_HOSTDB_PASSWORD 皆属於资料库的配置项目,但层级上与其他配置项目却是相同的,大致上会像这样:

{
  "DB_HOST": "example.com",
  "DB_PASSWORD": "12345678",
  "PORT": "3000"
}

我们的理想情况会是下方这样,相同类型的资料被归在一个 命名空间(Namespace) 里:

{
  "database": {
    "host": "example.com",
    "password": "12345678"
  },
  "port": "3000"
}

虽然无法在环境变数档做好这样的配置,但可以透过工厂函式来做处理。修改 configuration.factory.ts,透过 registerAs 这个函式来指定其命名空间,第一个参数即命名空间,第二个参数为 Callback,回传的内容即整理好的物件:

import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  host: process.env.DB_HOST,
  password: process.env.DB_PASSWORD
}));

修改 app.controller.ts,从程序码可以发现,如果要取出命名空间内的某项环境变数的话,透过 . 的方式取得即可,就跟操作 Object 资料一样:

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

  @Get()
  getHello() {
    const database = this.configService.get('database');
    const db_host = this.configService.get('database.host'); // 取得 database 里的 host
    const port = this.configService.get('PORT');
    return { database, db_host, port };
  }
}

透过浏览器查看 http://localhost:3000
https://ithelp.ithome.com.tw/upload/images/20210518/20119338hHKTELiVZP.png

在 main.ts 中使用 ConfigService

有时候会在环境变数档里配置 port,要能够使用环境变数档里的 port 作为启动 Nest 的 port,就必须在 main.ts 做处理,但要怎麽取得 ConfigService 呢?其实 app 这个实例有提供一个 get 方法,可以取出其参照:

import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService); // 取得 ConfigService
  const port = configService.get('port');
  await app.listen(port);
}
bootstrap();

环境变数档之扩展变数

假设有两个环境变数是存在依赖关系的,具体内容如下:

APP_DOMAIN=example.com
APP_REDIRECT_URL=example.com/redirect_url

可以看出 APP_REDIRECT_URL 包含了 APP_DOMAIN,但环境变数档并没有宣告变数的功能,这样在管理上会比较麻烦,还好 Nest 有实作一个功能来弥补,透过指定 forRoot 物件参数中的 expandVariablestrue 来解析环境变数档,让环境变数档像有变数宣告功能一样,透过${...} 来嵌入指定的环境变数。下方为 development.local.env 的内容:

APP_DOMAIN=example.com
APP_REDIRECT_URL=${APP_DOMAIN}/redirect_url

修改一下 app.module.ts 的内容:

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

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      expandVariables: true // 开启环境变数档变数嵌入功能
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

修改 app.controller.ts,让 getHello 回传 APP_DOMAINAPP_REDIRECT_URL

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

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

  @Get()
  getHello() {
    const app_domain = this.configService.get('APP_DOMAIN');
    const redirect_url = this.configService.get('APP_REDIRECT_URL');
    return { app_domain, redirect_url };
  }
}

透过浏览器查看 http://localhost:3000
https://ithelp.ithome.com.tw/upload/images/20210518/20119338OxAC5Njy1u.png

全域 ConfigModule

如果 ConfigModule 会在多个模组中使用的话,可以配置 isGlobaltrue 将其配置为全域模组,这样就不需要在其他模组中引入 ConfigModule

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

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['development.local.env', 'development.env'],
      isGlobal: true
    })
  ],
  controllers: [
    AppController
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

小结

环境变数的配置绝对是必要的,在有很多执行环境的情况下,更需要使用像 ConfigModule 这样的管理模组来降低维护成本。这里附上今天的懒人包:

  1. 官方有实作一套环境变数管理模组 - ConfigModule
  2. ConfigModule 使用 Dynamic Module 的概念实作。
  3. 透过 .env 档来配置环境变数。
  4. 透过 envFilePath 来指定自订的环境变数档。
  5. envFilePath 可以按照优先权做排序。
  6. 使用工厂函式与 load 搭配来处理环境变数。
  7. 运用工厂函式来配置命名空间,以归纳各个环境变数的类别。
  8. 可以在 main.ts 中取出 ConfigService 来获得环境变数。
  9. 透过 expandVariables 让环境变数档有嵌入变数的功能。
  10. 透过 isGlobalConfigModule 提升为全域模组。

<<:  [DAY17]模板确认

>>:  Day31 ( 游戏设计 ) 猴子接香蕉

【把玩Azure DevOps】Day3 Organization与Projects

首先,在网址列输入dev.azure.com这个网址,如果已经是有登入Microsoft Accou...

Day 22 - [API] 使用 PHP 执行 Python 脚本

嗨! 昨天终於结束了语料库模型建置的部分,再来就要建立 API 了。这个系统中我采用了一个比较特别的...

【LeetCode】Array

本文会提到做 array 常犯错误、如何避免,与常见的技巧。 此系列 Leetcode 篇不介绍基本...

机器学习:演算法

线性代数 LR:逻辑回归(Logistic Regression): 预测事件发生的机率(y=1)...