如何处理环境变量

node.js中有一个专门的 npm 包 dotenv 用来处理环境变量。

nest.js 对它进行封装,提供了一个内置包 @nest/config。按照官方文档,使用已经封装好的ConfigModule来处理环境配置文件。

首先安装依赖:

bash
1pnpm i --save @nestjs/config

app.module.ts中导入ConfigModule模块。

typescript
1import { Module } from '@nestjs/common'
2import { AppController } from './app.controller'
3import { AppService } from './app.service'
4import { ConfigModule } from '@nestjs/config'
5
6@Module({
7  imports: [ConfigModule.forRoot()],
8  controllers: [AppController],
9  providers: [AppService],
10})
11export class AppModule {}

env 文件的环境变量

创建.env 文件

yaml
1DATABASE_USER=test

app.service.ts中注入ConfigService并测试一下:

typescript
1import { Inject, Injectable } from '@nestjs/common'
2import { ConfigService } from '@nestjs/config'
3
4@Injectable()
5export class AppService {
6  constructor(
7    @Inject(ConfigService)
8    private readonly configService: ConfigService,
9  ) {}
10  getHello() {
11    const a = this.configService.get('DATABASE_USER')
12    return { a }
13  }
14}

访问一下可以看到结果:

image-20240217150208439

ConfigService会自动提取.env文件中的环境变量。

那如果我有多个环境变量的文件该怎么办?

再创建.env.dev文件

yaml
1DATABASE_PASSWORD=test_password

这样就得手动指定.env文件的 path 了

typescript
1    ConfigModule.forRoot({
2      envFilePath: ['.env.dev', '.env'],
3    }),

@nestjs/config会将多个文件的环境变量合并到一起。

修改一下代码:

typescript
1  getHello() {
2    const a = this.configService.get('DATABASE_USER');
3    const b = this.configService.get('DATABASE_PASSWORD');
4    return { a, b };
5  }

再访问一下:

image-20240217150308127

接下来还有一个问题,如果线上分为test环境、prod环境,并且这些环境的环境变量的值是不一样的,该如何处理?

我们再新建.env.test.env.prod两个文件。

其中.env.test的环境变量是这样的:

yaml
1APP_ENV=test

.env.prod的环境变量是这样的:

yaml
1APP_ENV=prod

这种情况就需要结合一个外部的NODE_ENV来实现了。

package.json中新增两条脚本:

json
1  "scripts": {
2    "dev:test":"NODE_ENV=test nest start --watch",
3    "dev:prod":"NODE_ENV=prod nest start --watch",
4    ...
5  },

这两个脚本的意思是在执行该脚本时,将NODE_ENV设置为test或者prod

接着修改 envFilePath:

typescript
1    ConfigModule.forRoot({
2      envFilePath: ['.env.dev', '.env', `.env.${process.env.NODE_ENV}`],
3    }),

现在envFilePath会再根据NODE_ENV的值去读取对应的环境变量文件。

如果执行pnpm dev:test,就会将.env.test文件给合并进来。

修改代码测试一下:

typescript
1  getHello() {
2    const a = this.configService.get('DATABASE_USER');
3    const b = this.configService.get('DATABASE_PASSWORD');
4    const appEnv = this.configService.get('APP_ENV');
5    return { a, b, appEnv };
6  }

查看结果:image-20240217152130584

如果执行的是pnpm dev:prod,就会将.env.prod文件合并进来。

查看结果:

image-20240217152239634

ts 文件的环境变量

使用.env文件配置环境变量不够灵活,能否用 ts 代码来生成环境变量?

@nestjs/config 也是支持的。

创建config.ts

typescript
1export default async () => {
2  const dbPort = await Promise.resolve(5432)
3  const dbHost = await Promise.resolve('localhost')
4  return {
5    db: {
6      host: dbHost,
7      port: dbPort,
8    },
9  }
10}

使用 load 加载 config.ts 导出的方法。

typescript
1...
2import config from './config';
3
4@Module({
5  imports: [
6    ConfigModule.forRoot({
7      envFilePath: ['.env.dev', '.env', `.env.${process.env.NODE_ENV}`],
8      load: [config],
9    }),
10  ],
11...
12})
13...

修改代码:

typescript
1  getHello() {
2    const a = this.configService.get('DATABASE_USER');
3    const b = this.configService.get('DATABASE_PASSWORD');
4    const appEnv = this.configService.get('APP_ENV');
5    const db = this.configService.get('db');
6    return { a, b, appEnv, db };
7  }

最后执行pnpm dev:prod并查看结果:

image-20240217154410340

通过这种方式可以动态加载环境变量。

设置 global service

创建一个resource

bash
1nest generate resource bbb --no-spec

在 Controller 中注入 ConfigService

typescript
1import { Controller, Get } from '@nestjs/common'
2import { ConfigService } from '@nestjs/config'
3
4@Controller('bbb')
5export class BbbController {
6  constructor(private readonly configService: ConfigService) {}
7
8  @Get('/')
9  async getHello() {
10    return this.configService.get('db')
11  }
12}

会得到一个报错:

image-20240217155830160

原因是现在的ConfigService还不是全局模块,仅仅只能在AppModule中使用。

将其修改为全局模块:

diff
1    ConfigModule.forRoot({
2      envFilePath: ['.env.dev', '.env', `.env.${process.env.NODE_ENV}`],
3      load: [config],
4+      isGlobal: true,
5    }),

现在访问一下http://localhost:3000/bbb看看结果:

image-20240217160052224

forFeature

nestjs约定, 动态模块的forRoot方法通常用于在 AppModule 里注册,一般用于指定全局模块

forFeature 用于局部配置在不同模块里 imports

而 register 用于一次性的配置。

如果我们想要在 bbbModule 中使用局部环境变量,则可以使用forFeature方法:

typescript
1  imports: [
2    ConfigModule.forFeature(() => {
3      return {
4        bbb: 'bbbbbb',
5      };
6    }),
7  ],

修改代码测试一下:

typescript
1  @Get('/')
2  async getHello() {
3    // return this.configService.get('db');
4    return this.configService.get('bbb');
5  }

访问http://localhost:3000/bbb查看结果:

image-20240217160922569

总结

Nodejs 使用 dotenv 读取环境变量。

Nestjs 对其进行封装提供了@nestjs/config包。

使用 ConfigModule.forRoot 可以读取 .env 配置文件,然后注入 ConfigService 来取配置。

还可以通过 ConfigModule.forFeature 来注册局部配置。

示例代码