前一篇我们运用 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 的概念来实作即可。
既然有造好的轮子可以使用,且前一篇也有简单的带过如何用 Dynamic Module 实作,故这篇就专门介绍官方实作的套件。该套件并不是内建的,需要额外安装,透过 npm
进行安装即可:
提醒:若前一篇有安装过
dotenv
可以先行移除。
$ npm install @nestjs/config --save
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
,在 AppController
的 contructor
注入 ConfigService
,让 getHello
透过 ConfigService
的 get
方法取出 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:
预设状态下,ConfigModule
会从专案路径下取 .env
档来做为环境变数档,但我们常常会需要为不同环境配置多个档案,这时候就可以透过自订环境变数档来处理。ConfigModule
的 forRoot
静态方法有提供 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:
还有一种情况是本地测试使用的环境变数与其他环境下测试用的环境变数不相同,这时候可以使用优先权的方式做载入,假设本地端使用的环境变数档名为 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 会看到 username
为 local_tester
:
有些复杂的情境可以透过工厂模式来处理环境变数,比如:假设有配置 development.env
,但有些比较不敏感的资讯可以直接使用预设值,故不需要在档案里面做相关配置,只需要在工厂函式里做配置即可。我们在 src
资料夹下创建一个名为 config
的资料夹,并在里面建立 configuration.factory.ts
:
修改 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
透过 ConfigService
的 get
方法取出 USERNAME
与 PORT
并回传:
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:
由於环境变数在配置的时候是采用 =
来划分 key
与 value
的,并不能在 value
的地方延伸出下一个层级,所以环境变数层级是 扁平 的,没有办法按照类别做归类,以下方为例,假设环境变数档 development.local.env
里面有下方资讯:
DB_HOST=example.com
DB_PASSWORD=12345678
PORT=3000
可以很明显看出 DB_HOST
与 DB_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:
有时候会在环境变数档里配置 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
物件参数中的 expandVariables
为 true
来解析环境变数档,让环境变数档像有变数宣告功能一样,透过${...}
来嵌入指定的环境变数。下方为 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_DOMAIN
与 APP_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:
如果 ConfigModule
会在多个模组中使用的话,可以配置 isGlobal
为 true
将其配置为全域模组,这样就不需要在其他模组中引入 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
这样的管理模组来降低维护成本。这里附上今天的懒人包:
ConfigModule
。ConfigModule
使用 Dynamic Module 的概念实作。.env
档来配置环境变数。envFilePath
来指定自订的环境变数档。envFilePath
可以按照优先权做排序。load
搭配来处理环境变数。main.ts
中取出 ConfigService
来获得环境变数。expandVariables
让环境变数档有嵌入变数的功能。isGlobal
让 ConfigModule
提升为全域模组。
(*Source Article - What is Model-View-Controller (...
首先,在网址列输入dev.azure.com这个网址,如果已经是有登入Microsoft Accou...
嗨! 昨天终於结束了语料库模型建置的部分,再来就要建立 API 了。这个系统中我采用了一个比较特别的...
本文会提到做 array 常犯错误、如何避免,与常见的技巧。 此系列 Leetcode 篇不介绍基本...
线性代数 LR:逻辑回归(Logistic Regression): 预测事件发生的机率(y=1)...