定制化配置系统
学习目标
- 构建一个灵活的配置系统
- 写一个数据库配置工具
预装依赖
文件结构
本节课目录和文件结构的更改只聚焦于core
包,编写好之后的目录结构如下
src/core
├── common
│ ├── configure
│ │ ├── base.util.ts
│ │ ├── configure.ts
│ │ ├── env.ts
│ │ ├── index.ts
│ │ └── utiler.ts
│ ├── constants.ts
│ ├── constraints
│ │ ├── index.ts
│ │ ├── match.constraint.ts
│ │ ├── match.phone.constraint.ts
│ │ └── password.constraint.ts
│ ├── core.module.ts
│ ├── decorators
│ │ ├── dto-validation.decorator.ts
│ │ └── index.ts
│ ├── helpers.ts
│ ├── index.ts
│ ├── providers
│ │ ├── app.filter.ts
│ │ ├── app.interceptor.ts
│ │ ├── app.pipe.ts
│ │ └── index.ts
│ └── types.ts
└── database
├── base
│ ├── data.service.ts
│ ├── index.ts
│ ├── repository.ts
│ ├── subscriber.ts
│ └── tree.repository.ts
├── constants.ts
├── constraints
│ ├── index.ts
│ ├── model.exist.constraint.ts
│ ├── tree.unique.constraint.ts
│ ├── tree.unique.exist.constraint.ts
│ ├── unique.constraint.ts
│ └── unique.exist.constraint.ts
├── db.util.ts
├── index.ts
├── parse-uuid-entity.pipe.ts
└── types.ts
应用编码
在开始编码之后创建supports/database
目录,并把原来的一些数据库相关的类型,常量,函数等放入此目录中的文件,目录结构如上面所示,然后更改一下应用中的导入地址
配置系统
由于官方提供的@nestjs/config
是通过系统内部的模块方式注入服务来实现的,很多场景下无法满足灵活的需求,比如自动化的多数据配置等,所以本节实现一个简单灵活的配置系统
类型及常量
配置中并非所有元素都是静态,一些属性可能通过函数获取,比如通过env
获取环境变量,而此函数在调用loadEnvs
加载环境文件前是无法获取所有我们自定义的环境变量的,所以必须把配置包含在一个回调函数中,读取的时候去执行通过返回值获取,因此我们定义一个ConfigRegister
作为获取配置的回调函数的类型
// src/core/common/types.ts
export interface AppConfig {
debug: boolean; // 是否debug
timezone: string; // 时区
locale: string; // 语言
port: number; // 服务器端口
https: boolean; // 是否通过https访问
host: string; // 服务器地址
}
export interface BaseConfig {
app: AppConfig;
[key: string]: any;
}
// 配置注册器函数
export type ConfigRegister<T> = () => T;
// 多个配置注册器集合
export type ConfigRegCollection<T> = {
[P in keyof T]?: () => T[P];
};
定义一个专门用于配置当前环境值的枚举常量
// src/core/common/constants.ts
export enum EnviromentType {
DEV = 'development',
PROD = 'production',
TEST = 'test',
}
环境变量
代码: src/core/supports/configure/env.ts
首先编写loadEnvs
函数,加载环境变量,步骤如下
当前环境通过启动命令中的
NODE_ENV
赋值,其它的process.env
环境变量都可以通过启动命令赋值如
cross-env NODE_ENV=development nest start
- 使用find-up向上层查找
.env
文件,直到找到为止,如果没有则undefined
- 使用find-up向上层查找
.env.当前环境
文件,直到找到为止,如果没有则undefined
- 把前面查找的两个文件地址放入一个数组并过滤掉
undefined
- 使用dotenv循环读取上面过滤后的文件数组中的环境变量,后者覆盖前者的变量,最终赋值给一个对象
- 把以上读取的自定义环境变量对象与
process.env
合并后赋值给一个对象,前者覆盖后者 - 把这个对象中的环境变量重新全部赋值给
process.env
编写env
函数来读取环境变量,env
函数通过重载实现多参数模式,其参数如下
key
: 需要获取的环境变量的名称parseTo
: 转义函数(由于环境变量读取后其值都是字符串类型,所以对于number
,boolean
等类型的值需要传入转义函数进行转义)defaultValue
: 默认值
// src/core/common/configure/env.ts
// 获取全部环境变量
export function env(): { [key: string]: string };
// 直接获取环境变量
export function env<T extends BaseType = string>(key: string): T;
// 获取类型转义后的环境变量
export function env<T extends BaseType = string>(
key: string,
parseTo: ParseType<T>,
): T;
// 获取环境变量,不存在则获取默认值
export function env<T extends BaseType = string>(
key: string,
defaultValue: T,
): T;
// 获取类型转义后的环境变量,不存在则获取默认值
export function env<T extends BaseType = string>(
key: string,
parseTo: ParseType<T>,
defaultValue: T,
): T;
// 获取环境变量的具体实现
export function env<T extends BaseType = string>(
key?: string,
parseTo?: ParseType<T> | T,
defaultValue?: T,
)
为了方便,写一个environment
函数用于获取当前环境NODE_ENV
配置类
配置类的实现比较简单,其属性和方法如下
_created
属性的作用是避免重复加载配置
// src/core/common/configure/configure.ts
export class Configure<T extends BaseConfig = BaseConfig> {
// 配置加载状态
protected _created = false;
// 根据配置注册器生成的配置
protected _config!: { [key: string]: any };
// 根据传入的配置构造器对象集生成所有配置
create(_config: ConfigRegCollection<T>)
// 配置是否已加载
get created()
// 获取一个配置,不存在则返回defaultValue
get<CT extends any = any>(key: string, defaultValue?: CT)
// 判断一个配置是否存在
has(key: string): boolean
// 获取所有配置
all<CT extends T = T>()
// 加载环境变量并重置所有配置
protected reset(_config: ConfigRegCollection<T>)
// 传入配置注册器集合并执行每个配置注册器来加载所有配置
protected loadConfig(_config: ConfigRegCollection<T>)
}
Util
模式
为了便捷的使用各种第三方服务和类库(如数据库,Redis,Socket,云存储等),我们构建一个简单的Util
机制,每种服务使用Util
类自动化配置以及在Util
中编写各种API方法
BaseUtil
添加一个用于配置映射的类型
// src/core/common/types.ts
export interface UtilConfigMaps {
required?: boolean | string[];
maps?: { [key: string]: string } | string;
}
这个所有Util
了基类,运行流程如下
- 通过
mapConfig
把configure
(前面创建的Configure
类的对象)中需要的配置映射到configMaps
属性 - 在映射的时候使用
checkAndGetConfig
,如果是required
是true
或是个包含此字段的数组,但是配置中又没有就抛出错误 - 最终在
factory
方法中调用mapConfig
获取映射后的配置并作为参数赋值给子类的create
方法,子类可以选择把配置赋值给config
属性或其它用途
// src/core/common/configure/base.util.ts
export abstract class BaseUtil<CT> {
protected _created = false;
protected configure!: Configure;
// 子类配置
protected config!: CT;
// 配置映射
protected abstract configMaps?: UtilConfigMaps;
/**
* 检测是否已被初始化
*/
created()
/**
* 始化Util类
* 将映射后的配置放入子类的factory进行进一步操作
* 比如赋值给this.config
*/
factory(configure: Configure)
/**
* 由子类根据配置初始化
*/
protected abstract create(config: any): void;
/**
* 根据configMaps获取映射后的配置
* 如果configs是一个string则直接在获取其在配置池中的值
* 如果configs是一个对象则获取后再一一映射
*/
private mapConfig()
/**
* 检测并获取配置
* 如果required为true则检测每个配置在配置池中是否存在
* 如果required为数组则只把数组中的值作为key去检测它们在配置池中是否存在
* 其它情况不检测
*/
protected checkAndGetConfig(
name: string,
key: string,
required?: UtilConfigMaps['required'],
)
}
DbUtil
添加用于数据库配置的类型
需要注意的是与
nestjs
默认的配置方式不同,为了让配置更加清晰,在自定义的配置方式中我们让每个连接必须带有连接名称
// src/core/database/types.ts
export interface DatabaseConfig {
// 数据库默认配置
default?: string;
// 启用的连接名称
enabled: string[];
// 数据库连接配置
connections: DbOption[];
// 所有连接的公共配置,最终会合并到每个连接中
common: Record<string, any>;
}
// 数据库连接配置
export type DbOption = TypeOrmModuleOptions & {
name: string;
};
DbUtil继承自BaseUtil
用于配置数据库连接,后面的教程会做更多处理,比如数据迁移和填充都要用到,目前比较简单,方法列表如下
getNestOptions
获取所有用于TypeOrmModule的数据库连接的配置,设置autoLoadEntities
为``true,使
entity在
autoLoadEntities后自动加载
entity
由于在
autoLoadEntities后自动加载,
subscriber`由提供者方式注册 所以在配置中去除这两者
后续教程我们会写一个自定义的模块创建器来处理
subscriber
等问题
getNestOption
根据名称获取一个用于Nestjs默认方式的TypeOrmModule的数据库连接配置
setOptions
根据配置设置连接
- 如果有设置默认连接则启用默认连接,否则选择enabled中的第一个连接为默认连接
- 如果enabled中没有添加默认连接的自动,则自动添加
- 检查所有enabled中的连接已配置
- 合并common配置到每个连接
getMeta
使用getNestOptions
获取所有配置,并提供给CoreModule
用于TypeOrmModule.forRoot
注册每个连接
Utiler
管理器
Utiler
用于管理所有的Util
类,并为它们创建[类名,对象]
的映射
此类有一个mergeMeta
专门用于合并Util
中通过getMeta
方法提供ModuleDataMeta
数据
// src/core/common/configure/utiler.ts
export class Utiler {
// 根据传入的configure对象和需要启用的utils进行初始化
create(configure: Configure, utils: Array<Type<BaseUtil<any>>>)
// Util是否存在
has<U extends BaseUtil<C>, C extends any>(name: Type<U>): boolean
// 根据Util类获取其对象
get<U extends BaseUtil<C>, C extends any>(name: Type<U>): U
// 合并ModuleMetaData数据
mergeMeta(meta: ModuleMetadata): ModuleMetadata {
const utilMetas: ModuleMetadata = this.utils
.map((u) => {
const v = u.value as any;
return typeof v.getMeta === 'function' ? v.getModuleMeta() : {};
})
.reduce(
(o, n) =>
merge(o, n, {
arrayMerge: (_d, _s, _o) => [..._d, ..._s],
}),
{},
);
return merge(meta, utilMetas, {
arrayMerge: (_d, _s, _o) => [..._d, ..._s],
});
}
}
核心模块
为了可以传入动态配置,需要把原来的CoreModule
改成动态模块
- 根据传入的配置初始化
Configure
类 - 将初始化后的
configure
对象用于传入utiler
用于创建各个util
对象 - 使用
utiler
的mergeMeta
合并各个Util
中提供给核心模块的元元素和默认的元元素(如果一些Util
中没有getMeta
方法则略过),对于DbUtil
将提供使用TypeOrmModule.forRoot
注册每个启用的连接的imports
// src/core/common/core.module.ts
@Module({})
export class CoreModule {
static forRoot<T extends BaseConfig = BaseConfig>(
configs: ConfigRegCollection<T>,
utils: Array<Type<BaseUtil<any>>>,
): DynamicModule {
const configure = new Configure();
configure.create(configs);
const utiler = new Utiler();
utiler.create(configure, utils);
const defaultMeta: ModuleMetadata = {
...
};
return {
module: CoreModule,
global: true,
...utiler.mergeMeta(defaultMeta),
};
}
}
配置文件
分别创建config/app.config.ts
和config/database.config.ts
对应用和typeorm数据库连接进行配置
然后在AppModule
中注册CoreModule
,通过forRoot
方法传入配置和需要启用的DbUtil
// src/app.module.ts
@Module({
imports: [CoreModule.forRoot(config, [DbUtil]), ContentModule, CoreModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
启动文件
最后美化一下输出的日志,去除原来的普通日志,只输出debug,错误信息和ip+端口
的格式
// src/main.ts
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
{ logger: ['error', 'warn', 'debug'] },
);
useContainer(app.select(AppModule), { fallbackOnErrors: true });
const configure = app.get(Configure, { strict: false });
const appConfig = configure.get<AppConfig>('app')!;
await app.listen(appConfig.port, appConfig.host, () => {
console.log();
console.log('Server has started:');
const listens: string[] = [];
const nets = networkInterfaces();
Object.entries(nets).forEach(([_, net]) => {
if (net) {
for (const item of net) {
if (item.family === 'IPv4') listens.push(item.address);
}
}
});
const urls = listens.map(
(l) =>
`{appConfig.https ? 'https' : 'http'}://{l}:{
appConfig.port
}`,
);
if (urls.length>0) {
console.log(`- Local:{green.underline(urls[0])}`);
}
if (urls.length > 1) {
console.log(`- Network: ${green.underline(urls[1])}`);
}
});
}
bootstrap();