这是《nestjs搭建通用业务框架》系列的第5篇进入开发具体的功能之前学习nest框架本身提供的CLI工具与规划合理的工程目录对于要实现的内容进行架构与计划这是实现健壮高可用的框架的前提。
技术整合会从三个层次进行介绍
数据库 -> 配置(多环境) -> 配置验证 -> 系统日志本篇
跨域 -> 错误拦截器 -> 缓存Redis
数据校验 -> 日志拦截 -> 鉴权
通过数据库集成库或 ORM
例如 Sequelize (recipe)和 TypeORM 以在更高的抽象级别上进行操作。
ORM对象关系映射英语Object Relational Mapping是一种程序设计技术用于实现面向对象编程语言里不同类型系统的数据之间的转换。 从效果上说它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。
应用场景
SQL -> DB我们写一套配置针对不同的数据库都可以方便的接入
DB -> SQL针对不同的数据库都可以通过抽象层进行联接
Nest
还提供了与现成的TypeORM
与@nestjs/typeorm
的紧密集成我们将在本章中对此进行介绍而与@nestjs/mongoose
的紧密集成将在官方的这一章中介绍
目前主要的ORM工具库与特点
typeorm跨库查询事务、TS支持支持数据库MySQL, MariaDB, Postgres, CockroachDB, SQLite, MSSQL, Oracle, SAPHana, sql.js, MongoDB
objection TS、事务、饥饿加载、数据效验基于knexjs支持数据库Postgres**, MSSQL, MySQL, MariaDB, SQLite3, **Oracle, Amazon Redshift
sequelize 有非官方的中文文档目前缺少核心的维护与开发。支持PostgreSQL, MySQL, MariaDB, SQLite, MSSQL
prisma后起之秀(官方文档写的很不错)SQL自动合并对接GraphQL客户端、服务端+数据管理GUI支持PostgreSQL, MSSQL, MySQL, SQLite
通过上面的简单对比目前来看TypeORM是nest
官方支持且推荐的可以来这里看看它的特性。
Postgre(MySQL)
步骤
安装@nestjs/typeorm
typeorm
安装nodejs侧的数据库驱动程序如mysql
pg
新建数据库配置文件配置数据库
在app.module.ts
引入数据库的配置文件调用TypeOrmModule.forRoot
方法
启动程序进行测试
安装依赖跳过数据库安装过程
1 | npm install --save @nestjs/typeorm typeorm pg |
按照上面的步骤创建文件src/config/database.config.ts
数据库配置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { TypeOrmModuleOptions } from '@nestjs/typeorm' export const typeOrmConfig: TypeOrmModuleOptions = { type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'changeme', database: 'demo-db', entities: [`${__dirname}/../entity/**/*.{js,ts}`], synchronize: false, logging: ["error"], } |
在src/app.module.ts
中添加TypeOrm
配置
1 2 3 4 5 6 7 8 9 10 11 12 | import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { typeOrmConfig } from './config/typeorm.config'; @Module({ imports: [ TypeOrmModule.forRoot(typeOrmConfig) ], providers: [], }) export class AppModule {} |
然后就可以使用npm run start:dev
来进行调试了。
MongoDB
非关系型数据库MongoDB
同样可以使用TypeORM
官方提供了@nestjs/mongoose
包所以我们来介绍两种集成方法
@nestjs/mongoose
安装依赖
1 | npm install --save @nestjs/mongoose mongoose |
配置app.module.ts
1 2 3 4 5 6 7 | import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; @Module({ imports: [MongooseModule.forRoot('mongodb://localhost:27017/test')], }) export class AppModule {} |
mongoose
库安装依赖
1 | npm install --save mongoose |
新建src/database/database.providers.ts
1 2 3 4 5 6 7 8 9 | import * as mongoose from 'mongoose'; export const databaseProviders = [ { provide: 'DATABASE_CONNECTION', useFactory: async (): Promise<typeof mongoose> => await mongoose.connect('mongodb://localhost:27017/test'), }, ]; |
新建src/database/database.module.ts
1 2 3 4 5 6 7 8 | import { Module } from '@nestjs/common'; import { databaseProviders } from './database.providers'; @Module({ providers: [...databaseProviders], exports: [...databaseProviders], }) export class DatabaseModule {} |
配置app.module.ts
1 2 3 4 5 6 7 8 9 | import { Module } from '@nestjs/common'; import { DatabaseModule } from './database/database.module.ts'; @Module({ imports: [ DatabaseModule, ], }) export class AppModule {} |
应用程序通常在不同的环境中运行根据环境DevelopmentProduction的不同应该使用不同的配置设置。
两种方法
使用@nestjs/config
来实现对.env
的key=value
对进行解析
使用config
库解析yaml
格式的文件
@nestjs/config
1 | npm i --save @nestjs/config |
配置src/app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot(), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
然后创建.env
文件
1 2 | DATABASE_USER=test DATABASE_PASSWORD=test123 |
下面来使用src/app.controller.ts
中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import { Controller, Get } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AppService } from './app.service'; @Controller() export class AppController { constructor( private readonly appService: AppService, private configService: ConfigService, ) {} @Get() getHello(): string { const dbUser = this.configService.get<string>('DATABASE_USER'); console.log(dbUser); // 这里来测试 return this.appService.getHello(); } } |
如果访问localhost:3000
即可以看到
1 2 3 4 5 6 7 8 | [Nest] 14039 - 2021/03/13 下午9:43:54 [NestFactory] Starting Nest application... [Nest] 14039 - 2021/03/13 下午9:43:54 [InstanceLoader] ConfigHostModule dependencies initialized +95ms [Nest] 14039 - 2021/03/13 下午9:43:54 [InstanceLoader] ConfigModule dependencies initialized +0ms [Nest] 14039 - 2021/03/13 下午9:43:54 [InstanceLoader] AppModule dependencies initialized +1ms [Nest] 14039 - 2021/03/13 下午9:43:54 [RoutesResolver] AppController {}: +7ms [Nest] 14039 - 2021/03/13 下午9:43:54 [RouterExplorer] Mapped {, GET} route +3ms [Nest] 14039 - 2021/03/13 下午9:43:54 [NestApplication] Nest application successfully started +2ms test |
从这里点进去我们发现ConfigModuleOptions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import { ConfigFactory } from './config-factory.interface'; export interface ConfigModuleOptions { cache?: boolean; isGlobal?: boolean; ignoreEnvFile?: boolean; ignoreEnvVars?: boolean; envFilePath?: string | string[]; encoding?: string; validate?: (config: Record<string, any>) => Record<string, any>; validationSchema?: any; validationOptions?: Record<string, any>; load?: Array<ConfigFactory>; expandVariables?: boolean; } |
所支持的参数。
我们可以利用envFilePath
配合NODE_ENV
来在不同的启动命令的时候使用不同的配置。
1 | npm i cross-env |
然后添加两个文件.env.development
与.env.production
比如.env.production
1 2 | DATABASE_USER=test1 DATABASE_PASSWORD=test123321 |
下面修改scripts
1 | "start:prod": "cross-env NODE_ENV=production node dist/main", |
可以设置app.module.ts
中默认是development
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; const envPath = `.env.${process.env.NODE_ENV || 'development'}`; console.log(' ~ file: app.module.ts ~ line 7 ~ envPath', envPath); @Module({ imports: [ ConfigModule.forRoot({ envFilePath: envPath, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
同样大家可以启动了测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ➜ npm run start:prod > nestjs-common-template@0.0.1 start:prod /Users/macos/Projects/nestjs/nestjs-common-template > cross-env NODE_ENV=production node dist/main ~ file: app.module.ts ~ line 7 ~ envPath .env.production [Nest] 14977 - 2021/03/13 下午11:10:13 [NestFactory] Starting Nest application... [Nest] 14977 - 2021/03/13 下午11:10:13 [InstanceLoader] ConfigHostModule dependencies initialized +34ms [Nest] 14977 - 2021/03/13 下午11:10:13 [InstanceLoader] ConfigModule dependencies initialized +1ms [Nest] 14977 - 2021/03/13 下午11:10:13 [InstanceLoader] AppModule dependencies initialized +1ms [Nest] 14977 - 2021/03/13 下午11:10:13 [RoutesResolver] AppController {}: +6ms [Nest] 14977 - 2021/03/13 下午11:10:13 [RouterExplorer] Mapped {, GET} route +3ms [Nest] 14977 - 2021/03/13 下午11:10:13 [NestApplication] Nest application successfully started +3ms test1 |
上面打印的test1
正是我们设置在.env.production
中的内容。
yaml
格式的配置步骤
下载js-yaml
与@types/js-yaml
1 2 | npm i js-yaml npm i -D @types/js-yaml |
创建配置config.yml
1 2 3 4 5 6 7 8 9 10 11 12 | http: host: 'localhost' port: 8080 db: postgres: url: 'localhost' port: 5432 database: 'yaml-db' sqlite: database: 'sqlite.db' |
配置自定义文件
configuration.ts
1 2 3 4 5 6 7 8 9 10 11 | import { readFileSync } from 'fs'; import * as yaml from 'js-yaml'; import { join } from 'path'; const YAML_CONFIG_FILENAME = 'config.yml'; const filePath = join(__dirname, YAML_CONFIG_FILENAME); export default () => { return yaml.load(readFileSync(filePath, 'utf8')); }; |
forRoot
中的load方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import Configuration from './config/configuration'; // 这里调整 @Module({ imports: [ ConfigModule.forRoot({ load: [Configuration], // load方法 }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
修改app.controller.ts
中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { Controller, Get } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AppService } from './app.service'; import { DatabaseConfig } from './interface'; @Controller() export class AppController { constructor( private readonly appService: AppService, private configService: ConfigService, ) {} @Get() getHello(): string { const db = this.configService.get<DatabaseConfig>('db'); console.log(db); return this.appService.getHello(); } } |
定义src/interface.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | export interface DatabaseConfig { postgres: PostgresConfig; sqlite: SqliteConfig; } export interface PostgresConfig { url: string; port: number; database: string; } export interface SqliteConfig { database: string; } |
最后测试
1 2 3 4 5 6 7 8 9 10 11 | [Nest] 16960 - 2021/03/13 下午11:34:00 [NestFactory] Starting Nest application... [Nest] 16960 - 2021/03/13 下午11:34:00 [InstanceLoader] ConfigHostModule dependencies initialized +30ms [Nest] 16960 - 2021/03/13 下午11:34:00 [InstanceLoader] ConfigModule dependencies initialized +0ms [Nest] 16960 - 2021/03/13 下午11:34:00 [InstanceLoader] AppModule dependencies initialized +0ms [Nest] 16960 - 2021/03/13 下午11:34:00 [RoutesResolver] AppController {}: +4ms [Nest] 16960 - 2021/03/13 下午11:34:00 [RouterExplorer] Mapped {, GET} route +3ms [Nest] 16960 - 2021/03/13 下午11:34:00 [NestApplication] Nest application successfully started +1ms { postgres: { url: 'localhost', port: 5432, database: 'yaml-db' }, sqlite: { database: 'sqlite.db' } } |
写到这里应该够用了代码可以查看本次提交。
config
库解析步骤
安装第三方包config
1 2 | npm i config -S npm i cross-env -D |
新建 配置文件config/default.json
同样还可以建立development.json
, production.json
1 2 3 4 5 | { "server": { "happy": "my default value" } } |
development.json
:
1 2 3 4 5 6 7 8 | { "server": { "port": 3001, "host": "localhost", "username": "test", "password": "test" } } |
production.json
:1 2 3 4 5 6 7 8 | { "server": { "port": 3002, "host": "localhost", "username": "prod", "password": "prod" } } |
app.controller.ts
中使用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import * as config from 'config'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { const server = config.get('server'); console.log(server); return this.appService.getHello(); } } |
1 2 | "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:prod": "cross-env NODE_ENV=production node dist/main", |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | ➜ npm run start:dev [Nest] 34580 - 2021/03/14 上午12:50:42 [NestFactory] Starting Nest application... [Nest] 34580 - 2021/03/14 上午12:50:42 [InstanceLoader] AppModule dependencies initialized +34ms [Nest] 34580 - 2021/03/14 上午12:50:42 [RoutesResolver] AppController {}: +6ms [Nest] 34580 - 2021/03/14 上午12:50:42 [RouterExplorer] Mapped {, GET} route +3ms [Nest] 34580 - 2021/03/14 上午12:50:42 [NestApplication] Nest application successfully started +2ms { happy: 'my default value', port: 3001, host: 'localhost', username: 'test', password: 'test' } ➜ npm run start:prod > nestjs-common-template@0.0.1 start:prod /Users/macos/Projects/nestjs/nestjs-common-template > cross-env NODE_ENV=production node dist/main [Nest] 34400 - 2021/03/14 上午12:50:03 [NestFactory] Starting Nest application... [Nest] 34400 - 2021/03/14 上午12:50:03 [InstanceLoader] AppModule dependencies initialized +71ms [Nest] 34400 - 2021/03/14 上午12:50:03 [RoutesResolver] AppController {}: +6ms [Nest] 34400 - 2021/03/14 上午12:50:03 [RouterExplorer] Mapped {, GET} route +2ms [Nest] 34400 - 2021/03/14 上午12:50:03 [NestApplication] Nest application successfully started +2ms { happy: 'my default value', port: 3002, host: 'localhost', username: 'prod', password: 'prod' } |
附上代码地址
配置验证
配置验证主要是指在应用程序启动时如果没有提供所需的环境变量或不符合某些验证规则就会抛出一个异常。@nestjs/config
包实现了两种不同的方式来实现这一点。
Joi
内置验证器。通过Joi你可以定义一个对象模式并根据它验证JavaScript对象
一个自定义的validate()
函数它将环境变量作为输入
特别说明
最新版本的joi
需要你运行Node v12或更高版本。旧版本的node请安装v16.1.8
。这主要是因为在v17.0.2
发布后在构建的时候会出现错误。更多信息请参考其17.0.0发布说明点击这里。
joi最好配合官方的@nestjs/config
进行使用
步骤
安装依赖
1 | npm install --save joi |
定义验证Schema
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import * as Joi from 'joi'; import { ConfigModule } from '@nestjs/config'; const envPath = `.env.${process.env.NODE_ENV || 'development'}`; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: envPath, // 这里多了一个属性validationSchema validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().default(3000), DATABASE_USER: Joi.string().required() }), }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
验证测试
配置错误
脚本
1 | "start:dev": "cross-env NODE_ENV=development PORT=toimc nest start --watch", |
1 | "start:dev": "cross-env NODE_ENV=development PORT=3000 nest start --watch", |
1 | npm run start:dev |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [下午7:33:38] Found 0 errors. Watching for file changes. /Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61 throw new Error(`Config validation error: ${error.message}`); ^ Error: Config validation error: "PORT" must be a number at Function.forRoot (/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61:23) at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/app.module.js:21:35) at Module._compile (internal/modules/cjs/loader.js:1063:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10) at Module.load (internal/modules/cjs/loader.js:928:32) at Function.Module._load (internal/modules/cjs/loader.js:769:14) at Module.require (internal/modules/cjs/loader.js:952:19) at require (internal/modules/cjs/helpers.js:88:18) at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/main.js:4:22) at Module._compile (internal/modules/cjs/loader.js:1063:30) |
.env.development
中的配置信息1 2 | DATABASE_USER= DATABASE_PASSWORD=test123 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61 throw new Error(`Config validation error: ${error.message}`); ^ Error: Config validation error: "DATABASE_USER" is not allowed to be empty at Function.forRoot (/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61:23) at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/app.module.js:21:35) at Module._compile (internal/modules/cjs/loader.js:1063:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10) at Module.load (internal/modules/cjs/loader.js:928:32) at Function.Module._load (internal/modules/cjs/loader.js:769:14) at Module.require (internal/modules/cjs/loader.js:952:19) at require (internal/modules/cjs/helpers.js:88:18) at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/main.js:4:22) at Module._compile (internal/modules/cjs/loader.js:1063:30) |
结论使用Joi
可以很方便对传入应用程序的参数进行验证可以限制传入的数据类型。
除了上面写的验证以外还可以加入以下属性来验证输入的命令参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Module({ imports: [ ConfigModule.forRoot({ envFilePath: envPath, validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().default(3000), DATABASE_USER: Joi.string().required() }), validationOptions: { // 这里加 allowUnknown: false, abortEarly: true, }, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
@nestjs/config
包使用的默认设置是
allowUnknown
控制是否允许在环境变量中使用未知键。默认为true
abortEarly
如果为true则在第一个错误时停止验证如果为false则返回所有错误。默认值为false。
注意上面的Joi的用法
主要是校验process.env
传入的参数
主要是校验envFilePath
初次加载的时候的参数
class-validator
步骤
安装依赖class-validator
与class-transformer
1 | npm i class-validator class-transformer |
配置效验文件src/env.validation.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import { plainToClass } from 'class-transformer'; import { IsEnum, IsNumber, validateSync } from 'class-validator'; enum Environment { Development = "development", Production = "production" } class EnvironmentVariables { @IsEnum(Environment) NODE_ENV: Environment; @IsNumber() PORT: number; } export function validate(config: Record<string, unknown>) { const validatedConfig = plainToClass( EnvironmentVariables, config, { enableImplicitConversion: true }, ); const errors = validateSync(validatedConfig, { skipMissingProperties: false }); if (errors.length > 0) { throw new Error(errors.toString()); } return validatedConfig; } |
app.module.ts
文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { validate } from './env.validation'; const envPath = `.env.${process.env.NODE_ENV || 'development'}`; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: envPath, validate, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
与使用Joi
验证结果一致。
使用第三方的包config
可以方便的读取配置信息但是校验却需要在读取的位置来加对于不需要验证而需要全局使用的配置项可以使用这种方式
官方的@nestjs/config
可以方便的导入.env
的文件同时结合js-yaml
也可以导入yaml
格式的配置。
配置灵活而且可以配合验证工具Joi
进行参数的验证推荐
自定义的校验第三方包class-validator
这里只是冰山一角后面在学习数据验证的时候还会使用到它