由于项目需求,需要将部分配置迁移到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,
};
},
});