NestJS博客实战03-Configuration配置

880 阅读5分钟

接下来,准备接入Log日志系统,NestJS有自带的Log系统但是,我希望自定义一些内容所以会进行改造。在此之前,需要先配置Configration,因为我希望在开发和生产的时候这套日志系统有不一样的行为。

Configuration 介绍

NestJS的Configuration,官方文档的2种配置方式,一种是用.env后缀文件,里面的内容是key/value形式,这和前端的环境配置有点像。另一种是yaml文件形式的,在这我采用yaml文件的方式。

Config 导入

参照官方的例子,尝试导入Configuration。

1. 安装依赖

pnpm i --save @nestjs/config

2. 注册ConfigModule

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

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

一旦注册了ConfigModule,NestJS就会默认读入.env文件里面的配置,并且添加到process.env变量中去。 举个例子.env里面的内容类似于下面这样

DATABASE_USER=test
DATABASE_PASSWORD=test

3. 自定义文件路径

通过设定envFilePath可以自定义文件路径

ConfigModule.forRoot({
  envFilePath: '.development.env',
});

// 或者 配置成多个数组的形式,当2个文件都有相同配置的时候,则第一个文件的配置优先
ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});

4. 警用环境变量加载

如果不想自动读入.env文件,则可以设置ignoreEnvFile参数为True。

ConfigModule.forRoot({
  ignoreEnvFile: true,
});

5. 使模块全局化

当您想在其他模块使用ConfigModule,则必须导入它。或者,通过设定isGlobaltrue来使它成为一个全局的模块

ConfigModule.forRoot({
  isGlobal: true,
});

6. 自定义配置文件

配置的内容也可以是一个对象,并且对象还可以设定默认值。

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432
  }
});

load参数来导入这个对象。

import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
})
export class AppModule {}

YAML的配置文件类似于下面这种形式

SERVER_VALUE:
  host: '0.0.0.0'
  port: 8000
LOG_CONFIG:
  TIMESTAMP: true
  LOG_LEVEL: 'info'
  LOG_ON: true

为了能够读入YAML文件,需要下载js-yaml

pnpm i js-yaml
pnpm i -D @types/js-yaml

然后通过yaml#load方法来加载YAML文件

// config/configuration.ts
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';

// 获取项目运行环境
export const getEnv = () => {
  return process.env.RUNNING_ENV;
};

export const getConfig = (type?: string) => {
  const environment = getEnv();
  const yamlPath = join(process.cwd(), `./.config/.${environment}.yaml`);
  const config = yaml.load(readFileSync(yamlPath, 'utf8')) as Record<
    string,
    any
  >;
  if (type) {
    return config[type];
  }
  return config;
};
注意:
在构建过程中,Nest CLI不会自动将您的“assets”(非TS文件)移动到dist文件夹。为了确保您的YAML文件被复制,您必须在nest-cli.json文件的acompilerOptions#assets对象中指定这一点。例如,如果config文件夹与src文件夹处于同一级别,则添加compilerOptions#assets,其值为“assets”:[{”include“:”../config/*.yaml“,”outDir“:“./dist/config”}]

7. 使用ConfigService

上面的准备工作就绪以后我们就可以通过ConfigService来获取yaml的内容了。前提是必须要导入ConfigModule模块(如果在ConfigModule.forRoot()里面设定了isGlobal的情况不需要导入)。

@Module({
  imports: [ConfigModule],
  // ...
})

然后就可以通过构造函数来注入了

constructor(private configService: ConfigService) {}

并且在我们的类中来使用他们

// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');

// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');

如上所示,使用configService.get()方法通过传递变量名来获得一个简单的环境变量。您可以通过传递类型来进行TypeScript类型提示,如上文所示(例如,get<string>(…))。get()方法还可以遍历嵌套的自定义配置对象(通过自定义配置文件创建),如上文第二个示例所示。

您还可以使用接口作为类型提示来获取整个嵌套的自定义配置对象

interface DatabaseConfig {
  host: string;
  port: number;
}

const dbConfig = this.configService.get<DatabaseConfig>('database');

// you can now use `dbConfig.port` and `dbConfig.host`
const port = dbConfig.port;

可以通过get()方法的第二个可选参数,来设定默认值

// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');

ConfigService有两个可选的泛型(类型参数)。第一个是帮助防止访问不存在的配置属性。按如下所示使用:

interface EnvironmentVariables {
  PORT: number;
  TIMEOUT: string;
}

// somewhere in the code
constructor(private configService: ConfigService<EnvironmentVariables>) {
  const port = this.configService.get('PORT', { infer: true });

  // TypeScript Error: this is invalid as the URL property is not defined in EnvironmentVariables
  const url = this.configService.get('URL', { infer: true });
}

infer推断属性设置为true后,ConfigService#get方法将根据接口自动推断属性类型,例如,typeof port===“number”(如果您没有使用TypeScript中的strictNullChecks标志),因为portEnvironmentVariables接口中有一个数字类型。 此外,使用推断功能,您可以推断嵌套的自定义配置对象的属性的类型,即使使用点表示法也是如此,如下所示:

constructor(private configService: ConfigService<{ database: { host: string } }>) {
  const dbHost = this.configService.get('database.host', { infer: true })!;
  // typeof dbHost === "string"                                          |
  //                                                                     +--> non-null assertion operator
}

第二个泛型依赖于第一个泛型,作为类型断言来消除ConfigService的方法在strictNullChecks打开时可以返回的所有undefined类型。例如:

// ...
constructor(private configService: ConfigService<{ PORT: number }, true>) {
  //                                                               ^^^^
  const port = this.configService.get('PORT', { infer: true });
  //    ^^^ The type of port will be 'number' thus you don't need TS type assertions anymore
}

命名空间, 缓存,部分登记这些大家可以了解一下这里不多做介绍了。

8. 结构验证

官方的结构验证有2种方式。Joi自定义validate().

这里我选择用Joi的方式来验证env。想更多了解自定义validate()Joi的朋友可以去看官网

首先安装Joi依赖

pnpm i joi --save

configuration.ts文件里面加入下面,有错误的情况暂时console.log出来。Joi方法, JoiOption参数

import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';
import * as Joi from 'joi';

/**
 * 对象验证逻辑
 */
const shcema = Joi.object().keys({
  SERVER_VALUE: Joi.object({
    port: Joi.number().default(8000),
    host: Joi.string().required(),
  }),
  LOG_CONFIG: Joi.object({
    TIMESTAMP: Joi.boolean().default(false),
    LOG_LEVEL: Joi.string().valid('info', 'error').required(),
    LOG_ON: Joi.boolean().default(false),
  }),
});

/**
 * 去的环境配置内容
 * @returns 环境配置
 */
export const getConfig = () => {
  // ...
  try {
    const { value, error } = shcema.validate(config);
    if (error) {
      console.log(error);
    }
    return value;
  } catch (error) {
    console.log(error);
    return {};
  }
};

9. 在main.ts中使用

通过app.get()可以在main.ts中获取configService.

async function bootstrap() {
  // ...

  const configService = app.get(ConfigService);
  const serverValue = configService.get('SERVER_VALUE');
  await app.listen(serverValue.port, serverValue.host);
}
bootstrap();

本章代码

代码