一定要看官方文档
nacos: https://nacos.io/zh-cn/docs/what-is-nacos.html
nacos-nodejs: https://github.com/nacos-group/nacos-sdk-nodejs
nestjs config: https://docs.nestjs.com/techniques/configuration#configuration
首先我们的项目会分为几个环境: 本地\测试\线上, 之前的项目代码配置都是根据 ts 文件中写死固定的. 但这样会带来新的问题, 配置无法灵活获取, 配置信息暴露不是很安全, 新增配置时需要新增修改代码配置文件.
所以在之后, 项目整体采用了 nestjs 的 config 配置, 统一配置管理, 依赖注入的方式去管理配置.
但后续发现, 在采用环境变量的方式去注入配置, 让nestjs config 管理时, 带来了新的问题. 环境变量在服务器中保存的越来越多, 更加不好管理, 配置.env 文件时, 也会有此问题. 于是将 nacos 集成至了nestjs, 用 nacos 来统一管理配置信息.
所以这篇文章, 主要记录的就是 nacos 与 config 在 nestjs 中的应用.
安装等基础的操作, 需要查看 [官方文档][https://docs.nestjs.com/techniques/configuration#configuration]
整个的项目配置流程大概是这样的:
- 服务器或者项目设置环境变量: NACOS_HOST, NACOS_PORT, PROJECT_ENV
- 项目启动后, nestjs config 模块会异步连接 nacos, 获取其中的配置信息
- nacos 上会根据 PROJECT_ENV 来区分不同的环境, 这样就可以多环境配置
- config 获取配置信息后, 会load config 中
- 使用 config 时, 需要依赖注入至全局 module 中, ConfigService 即可使用
如下代码省略了部分模块, 只记录 config 相关的使用.
- server_pro
- config
- configurations
env.configuration.ts
nacos.configuration.ts
core.configuration.ts
config.interface.ts
config.module.ts
- src
- apps
- logger
- packages
- nacosCli
nacos.manage.ts
app.module.ts
main.ts
import { ConfigService } from '@nestjs/config'
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule)
// 在 main 中使用, 直接 app.get() 即可
const configService = app.get(ConfigService)
console.log(configService)
const options = new DocumentBuilder()
.setTitle('Vmp Server')
.setDescription('The VMP Server API description')
.setVersion('0.1')
.addTag('vmp')
.build()
const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('api', app, document)
await app.listen(configService.get('HttpServerConfig').port)
if (module.hot) {
module.hot.accept()
module.hot.dispose(() => app.close())
}
}
bootstrap()
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigService } from '@nestjs/config'
@Module({
imports: [
// 这里自定义了 config 模块
VmpConfigModule,
// 使用的时候, 需要异步注入 typeorm 中
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('MysqlConfig'),
inject: [ConfigService]
}),
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { loadNacosConfig } from './configurations/nacos.configuration'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
cache: true,
// 由于 nacos 需要异步获取, 这里使用 load 的异步方式.
load: [async () => loadNacosConfig()]
// 这里可以用指定 .env 文件的方式
// load: [loadEnvConfig]
})
]
})
export class VmpConfigModule {}
用于连接 nacos, 进行配置信息的获取
import * as nacos from 'nacos'
export class NacosManager {
private client: any
private DATA_ID = ''
private GROUP = ''
private NAMESPACE = process.env.PROJECT_ENV ? process.env.PROJECT_ENV : 'local'
private SERVER_ADDR = `${process.env.NACOS_HOST}:${process.env.NACOS_PORT}`
constructor() {
if (process.env.NACOS_HOST && process.env.NACOS_PORT) {
this.getClient()
}
}
private async getClient() {
this.client = new nacos.NacosConfigClient({
serverAddr: this.SERVER_ADDR,
namespace: this.NAMESPACE,
requestTimeout: 6000
})
}
public async getAllConfig() {
const content = await this.client.getConfig(this.DATA_ID, this.GROUP)
return JSON.parse(content)
}
}
异步获取 nacos 的配置信息方法文件
import { envVariatesConfigDTO, VmpProjectConfigDTO } from '../config.interface'
import { NacosManager } from '../../src/packages/nacosCli/nacos.manage'
import { SerializeLoadConfig } from './core.configuration'
export const loadNacosConfig = async (): Promise<VmpProjectConfigDTO> => {
const configManager = new NacosManager()
const nacosConfigs: envVariatesConfigDTO = await configManager.getAllConfig()
return SerializeLoadConfig(nacosConfigs)
}
指定 .env 文件时, 可以用它来获取
import { VmpProjectConfigDTO } from '../config.interface'
import { SerializeLoadConfig } from './core.configuration'
export const loadEnvConfig = (): VmpProjectConfigDTO => {
return SerializeLoadConfig(process.env)
}
这里将处理环境变量的方法抽出来了, 统一管理
import { envVariatesConfigDTO, VmpProjectConfigDTO } from '../config.interface'
import { DbLogger } from '../../src/logger/log4js.core'
export const SerializeLoadConfig = (configs: envVariatesConfigDTO | any): VmpProjectConfigDTO => {
return {
ProjectName: configs.PROJECT_NAME,
ProjectENV: configs.PROJECT_ENV,
HttpServerConfig: {
port: Number(configs.SERVER_PORT),
baseUrl: configs.SERVER_BASE_URL
},
MysqlConfig: {
host: configs.VMP_MYSQL_HOST,
port: Number(configs.VMP_MYSQL_PORT),
username: configs.VMP_MYSQL_USERNAME,
password: configs.VMP_MYSQL_PASSWORD,
database: configs.VMP_MYSQL_DATABASE,
type: 'mysql',
timezone: '+08:00',
logger: new DbLogger(),
maxQueryExecutionTime: 1000,
entities: ['dist/**/*.entity{.ts,.js}']
},
AliyunOssConfig: {
options: {
accessKeyId: configs.OSS_ALIYUN_ACCESS_KEY,
accessKeySecret: configs.OSS_ALIYUN_ACCESS_SECRET,
bucket: configs.OSS_ALIYUN_BUCKET,
region: configs.OSS_ALIYUN_REGION,
secure: Boolean(configs.OSS_ALIYUN_SECURE)
},
pathPrefix: configs.OSS_ALIYUN_PATH_PREFIX
},
permissionLimit: configs.PERMISSION_LIMIT,
ApiServers: {
vmp_api_upload: configs.VMP_API_UPLOAD,
vmp_api_pychart: configs.VMP_API_PYCHART,
vmp_api_schart: configs.VMP_API_SCHART,
vmp_api_pdf2svg: configs.VMP_API_PDF2SVG,
vmp_api_nls: configs.VMP_API_NLS
},
RedisConfig: {
host: configs.VMP_REDIS_HOST,
port: Number(configs.VMP_REDIS_PORT),
password: configs.VMP_REDIS_PASSWORD,
exp: Number(configs.VMP_REDIS_EXP)
},
ServiceHost: configs.SERVER_HOST_URL
}
}
参数的类型定义可能不是很优雅, 后续再修改…
import { ConnectionOptions } from 'typeorm'
interface HttpServerConfig {
port: number
baseUrl: string
}
export interface AliyunOssOptions {
accessKeyId: string
accessKeySecret: string
bucket: string
region: string
secure: boolean
}
export interface AliyunOssConfig {
options: AliyunOssOptions
pathPrefix: string
}
export interface ApiServersConfig {
vmp_api_upload: string
vmp_api_pychart: string
vmp_api_schart: string
vmp_api_pdf2svg: string
vmp_api_nls: string
}
interface RedisConfig {
host: string
port: number
password: string
exp: number
}
// vmp 项目中使用的 config 结构体
export interface VmpProjectConfigDTO {
ProjectName: string
ProjectENV: string
HttpServerConfig: HttpServerConfig
MysqlConfig: ConnectionOptions
AliyunOssConfig: AliyunOssConfig
permissionLimit: string
ApiServers: ApiServersConfig
RedisConfig: RedisConfig
ServiceHost: string
}
// 环境变量的结构体
export interface envVariatesConfigDTO {
PROJECT_NAME: string
PROJECT_ENV: string
SERVER_PORT: number
SERVER_BASE_URL: string
VMP_MYSQL_HOST: string
VMP_MYSQL_PORT: number
VMP_MYSQL_USERNAME: string
VMP_MYSQL_PASSWORD: string
VMP_MYSQL_DATABASE: string
OSS_ALIYUN_ACCESS_KEY: string
OSS_ALIYUN_ACCESS_SECRET: string
OSS_ALIYUN_BUCKET: string
OSS_ALIYUN_REGION: string
OSS_ALIYUN_SECURE: boolean
OSS_ALIYUN_PATH_PREFIX: string
PERMISSION_LIMIT: 'CRM' | 'ALL'
VMP_API_UPLOAD: string
VMP_API_PYCHART: string
VMP_API_SCHART: string
VMP_API_PDF2SVG: string
VMP_API_NLS: string
VMP_REDIS_HOST: string
VMP_REDIS_PORT: number
VMP_REDIS_PASSWORD: string
VMP_REDIS_EXP: number
SERVER_HOST_URL: string
}
如果想要在项目模块中使用, 参考官方文档即可
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
// 因为是全局模块, 所以不需要再 import 了, 但官方好像不是很推荐全局