nestjs nacos+nest-typed-config 实现异步配置加载

由于项目需求,需要将部分配置迁移到nacos进行统一管理.便于维护和推送更改,所以需要改造nestjs项目.

问题点如下:

  • @nest/config 不支持异步加载配置
  • 采用异步加载后无法控制加载顺序

为解决以上两个问题,接住了社区方案:

nest-typed-config

支持异步加载,支持多种格式的配置文件,功能强大

配置文件

conf/env.development.yaml

nodeEnv: development
port: 4002
host: 0.0.0.0
prefix: server
nacos:
  server: 127.0.0.1:8848
  namespace: hg_gugong
  configRequestTimeout: 6000
  account: nacos
  password: nacos
  configId: development
  group: DEFAULT_GROUP

配置声明

必须要有最少一个class-validator 验证,不然会报验证错误.
src/config/config.scheam.ts

// src/config/config.scheam.ts
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional, IsString } from 'class-validator';

import { ConfigUtils } from '../utils/config.utils';

/**
 * 数据库配置
 */
export class TypeOrmOptionConfig {
  @IsString({
    message: 'pgConnection 数据库链接名称',
  })
  name: string = 'pgConnection';

  @IsString({
    message: 'mysql|postgres|oracle',
  })
  type: 'mysql' | 'postgres' | 'oracle';

  @IsString({
    message: 'host',
  })
  host: string;

  @IsString({
    message: 'port',
  })
  port: number;

  @IsString({
    message: 'root',
  })
  username: string;

  @IsString({
    message: 'pwd',
  })
  password: string;

  @IsString({
    message: 'database name',
  })
  database: string;

  @IsString({
    message: 'public',
  })
  schema: string;

  @IsBoolean({
    message: '自动同步',
  })
  synchronize: boolean;

  @IsBoolean({
    message: '是否开启日志',
  })
  logging: boolean;
}

/**
 * nacos 配置
 */
export class NacosOptionConfig {
  public server?: string;

  public namespace?: string;

  public configRequestTimeout?: number;

  public account?: string;

  public password?: string;

  public configId?: string;

  public group?: string;
}

/**
 * nacosValue.graphqlOption
 */
export class GraphqlOptionType {
  @IsString({
    message: '自动生成schema路径',
  })
  autoSchemaFile: string = 'src/graphql/schema.gql';

  @IsBoolean({
    message: '是否开启调试工具 default:false',
  })
  playground: boolean = false;

  @IsString({
    message: 'graphql router: 域名/path',
  })
  path: string = 'gql';

  @IsBoolean({
    message: '是否允许跨域',
  })
  cors: boolean = false;
}

export class NacosValueType {
  @IsString({
    message: '项目名称',
  })
  projectName: string;

  @IsString({
    message: 'jwt 密钥',
  })
  jwtSecret: string;

  @IsString({
    message: 'jwt 过期时间',
  })
  expiresIn: string;

  @IsString({
    message: '密码 key',
  })
  secretKey: string;

  @IsString({
    message: '密码 偏移量',
  })
  secretIv: string;

  @Type(() => GraphqlOptionType)
  graphqlOption?: GraphqlOptionType;

  @Type(() => TypeOrmOptionConfig)
  typeOrmOptionConfig?: TypeOrmOptionConfig;
}

/**
 * config schema
 * 必须要有 一个 validator
 */
export class RootConfig extends ConfigUtils {
  @IsOptional()
  @IsString()
  public nodeEnv?: string;

  @IsOptional()
  public port?: number;

  @IsOptional()
  @IsString()
  public host?: string;

  @IsOptional()
  @IsString({
    message: '系统前缀',
  })
  prefix: string = 'wzc';

  @IsOptional()
  @Type(() => NacosOptionConfig)
  public nacos?: NacosOptionConfig;

  @IsOptional()
  @Type(() => NacosValueType)
  public nacosValue?: NacosValueType;

  // @Type(() => DataBaseConfig)
  // public dataBaseConfig?: DataBaseConfig;
}

自定义module返回配置

src/config/config.module.ts

// src/config/config.module.ts

import { DynamicModule } from '@nestjs/common';
import { fileLoader, selectConfig, TypedConfigModule } from 'nest-typed-config';
import { typedConfigLoadNacos } from 'src/utils/config.utils';

import { RootConfig } from './config.schema';

export const ConfigModule: DynamicModule = TypedConfigModule.forRoot({
  schema: RootConfig,
  load: [
    fileLoader({
      // 命名规则, [env.(process.env.NODE_ENV).格式]
      basename: 'env',
      // 配置文件 文件夹目录名称
      searchFrom: 'conf',
    }),
    typedConfigLoadNacos,
  ],
});

/**
 * 提前使用
 */
export const rootConfig = selectConfig(ConfigModule, RootConfig);

typedConfigLoadNacos

动态load方法
src/utils/config.utils.ts

import { Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { get } from 'lodash';
import { NacosConfigClient } from 'nacos';
import { rootConfig } from 'src/config/config.module';
import { RootConfig } from 'src/config/config.schema';

import { EmitterUtils } from './emitter.utils';

/**
 * nacos加载 配置 订阅
 * @param value
 * @returns
 */
export const typedConfigLoadNacos = async (value: RootConfig) => {
  const logger = new Logger('typedConfigLoadNacos');
  // 如果没有配置 nacos 地址 则返回
  if (!value.nacos?.server) {
    // 事件通知完成 延迟加载配置完成
    EmitterUtils.emitLoadConfigEnd(value);
    return undefined;
  }
  const configClient = new NacosConfigClient({
    serverAddr: value.nacos.server,
    namespace: value.nacos.namespace,
    requestTimeout: value.nacos.configRequestTimeout,
    username: value.nacos.account,
    password: value.nacos.password,
  });
  const configJson = await configClient.getConfig(
    value.nacos.configId,
    value.nacos.group,
  );
  // 订阅 nacos 配置更改事件推送
  configClient.subscribe(
    { dataId: value.nacos.configId, group: value.nacos.group },
    (content: string) => {
      // 我采用了json格式 如果是其他格式可以自行处理生成
      const nacosValue = JSON.parse(content);
      // 合并配置
      Object.assign(rootConfig, { nacosValue });
      logger.debug(JSON.stringify(rootConfig), 'subscribe');
      // 事件通知完成 延迟加载配置完成
      EmitterUtils.emitLoadConfigEnd(rootConfig);
    },
  );
  return { nacosValue: JSON.parse(configJson) };
};

/**
 * config base
 */
export class ConfigUtils {
  constructor() {}
  // 增加 get方法
  get<T = string>(key: string) {
    return get(this, key) as T;
  }
}

EmitterUtils

全局事件 eventbus

import { EventEmitter2 } from '@nestjs/event-emitter';
import { RootConfig } from 'src/config/config.schema';

/**
 * 事件常量
 */
export const EventEmitterKey = {
  CONFIG: {
    /**
     * nacos 加载完成事件
     */
    TYPED_CONFIG_LOAD_END: 'config.typedConfigLoadEnd',
  },
};

/**
 * 事件管理器
 */
export class EmitterUtils {
  public static eventEmitter: EventEmitter2 = new EventEmitter2();

  /**
   * 订阅加载完成事件
   * @param success
   */
  public static subscribeLoadConfigEnd(): Promise<RootConfig> {
    return new Promise((resolve) => {
      this.eventEmitter.on(
        EventEmitterKey.CONFIG.TYPED_CONFIG_LOAD_END,
        (rootConfig) => resolve(rootConfig),
      );
    });
  }

  /**
   * 加载完成 通知
   */
  public static emitLoadConfigEnd(rootConfigValue?: RootConfig) {
    this.eventEmitter.emit(
      EventEmitterKey.CONFIG.TYPED_CONFIG_LOAD_END,
      rootConfigValue,
    );
  }
}

使用方式

// src/app.module.ts
import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
import { GraphqlModule } from './graphql/graphql.module';
import { LoggerMiddleware } from './middleware/logger.middleware';
import { ServerModule } from './server/server.module';
import { TypeormModule } from './typeorm/typeorm.module';
import winstonLogger from './utils/winston-logger.utils';

@Module({
  imports: [
    ConfigModule,
    GraphqlModule,
    ServerModule,
    WinstonModule.forRoot({
      transports: winstonLogger.transports,
      format: winstonLogger.format,
      defaultMeta: winstonLogger.defaultMeta,
      exitOnError: false, // 防止意外退出
    }),
    TypeormModule,
  ],
  controllers: [AppController],
  providers: [AppService, Logger],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

graphql 延迟等待config 加载

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { DynamicModule } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { join } from 'path';
import { EmitterUtils } from 'src/utils/emitter.utils';

export const GraphqlModule: DynamicModule =
  GraphQLModule.forRootAsync<ApolloDriverConfig>({
    driver: ApolloDriver,
    useFactory: async () => {
      const newConfig = await EmitterUtils.subscribeLoadConfigEnd();
      const graphqlOption = newConfig?.nacosValue?.graphqlOption;
      return {
        autoSchemaFile: join(process.cwd(), graphqlOption.autoSchemaFile),
        sortSchema: true,
        playground: graphqlOption.playground,
        path: graphqlOption.path,
        // resolvers: { JSON: GraphQLJSON },
        cors: true,
      };
    },
  });

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容