前言
需求和技术方案都已经整清楚了,项目的大方向已经确定了,就可以开始着手于开发了,这时候就需要根据之前写 服务端技术方案设计文档 来进行后端服务的开发了。
这里将会使用 Node.js 作为开发语音,使用 Nest.js 框架进行项目开发。
这里会一步一步的搭建出项目的架子,但是不会特意的去讲解 Nest.js 的语法。感兴趣的话可以去 Nest.js 官网 去看看文档。
安装 Nest Cli
脚手架这东西都屡见不鲜了,Nest 的脚手架除了可以快速创建项目模板外也可以快速创建各种 模块 文件,并且会自动引用还是相当便利的。
在安装 Nest Cli 之前,电脑需要安装 Node.js ,这都是基操了把?在 Node.js 的官网把安装包下载下来运行,然后下一步下一步就好了。
可以在 命令行工具 中执行 npm install -g @nestjs/cli
命令进行 Nesj Cli
的全局安装;
执行 npm update -g @nestjs/cli
命令可以对全局环境下的 Nesj Cli
进行更新,之前安装过的建议可以执行一次;
在安装了 Nesj Cli
后,可以执行 nest -h
查看一下 Nesj Cli
拥有哪些功能。
稍微简单的介绍一下主要的命令:
nest new
用来快速创建项目模板nest build
用来进行代码构建nest start
用来启动项目进行服务开发和代码调试的nest generate
用来快速生成各种模块的代码
其中 nest generate
命令需要稍微深入一下,可以执行 nest generate -h
更进一步的查看命令帮助信息。
稍微简单介绍几个主要的命令行为:
nest generate controller
创建控制器nest generate filter
创建过滤器(一般用来过滤异常抛出的)nest generate guard
创建路由守卫nest generate interceptor
创建拦截器nest generate middleware
创建中间件nest generate module
创建模块nest generate pipe
创建管道nest generate resource
创建 CRUD 的资源(简单说就是一个模块一个控制器和一个服务,这个会用的比较多吧)
创建基础项目模板
这里就可以直接使用 Nest Cli
的 new
命令来创建最初的项目框架。
首先需要在命令行工具中执行 nest new service
,然后根据你自己的包管理工具的倾向选择管理包管理工具,我这里选择的是 pnpm
:
然后基础的项目模板就创建好了:
目录也非常的简单,一些的配置文件以及 test
目录和 .spec.ts
的单元测试的内容都可以不用管,只用看 src
下面的内容,基本上就只是 main.js
作为服务的入口文件,然后有一个叫 app
的模块,该模块有一个控制器和服务而已:
注:其实在创建的时候是可以标注不创建 单元测试 相关的文件的,虽然接下来我也不会去写 单元测试 的内容,但是这一块的文件最好还是预留下来,服务端的单元测试还是很重要的。
整理目录
如下图所示哈,这里是我个人的习惯,你们也可以按照自己的习惯来定制。
- exceptions 是异常处理
- interceptor 是拦截器
- interface 是接口
- logger 是日志
- utils 是工具
- app.module.ts 是全局模块
- docs.ts 是 swagger
- main.ts 是入口文件
准备工作
目前项目的基础模板已经创建好了,目录也规划好了,那么还有一些准备工作要做,例如:日志、异常处理、返回值统一、环境变量、接口版本、接口文档等等,接下来就着手这些了。
接口版本处理
接口的版本管理本质上其实是做兼容方面的处理,在需求开发中避免不了的是不断的迭代和升级,那么在需要兼容一些老项目的情况下,就需要用到接口的版本管理,避免造成老项目的一个崩溃。
开启接口版本其实很简单,nest 本身已经内置了该功能,只需要在 main.ts 里面设置 app.enableVersioning(VersioningOptions) 方法即可。
这里我设置了默认版本为 VERSION_NEUTRAL 表示未指定版本的请求可以访问不受版本约束的路由,怎么约束路由的版本只需要用到装饰器 @Version(version) 即可。
整体代码如下所示:
javascript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VERSION_NEUTRAL, VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局版本控制
app.enableVersioning({ defaultVersion: [VERSION_NEUTRAL], type: VersioningType.URI });
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
规范化全局返回数据格式
在没有进行任何配置的时候,接口返回的数据是不标准的,在控制器里面 return 什么数据就会返回什么数据,这样不太合理也容易出问题,前端也不好做统一的接口数据处理,然而每个接口都要处理一次返回值格式也不太合理,而且也容易会有疏漏,这里就需要用到拦截器了。
首先需要在 src/interceptors 目录下创建一个 transform.interceptor.ts 的文件,具体代码如下:
kotlin
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface IResponse<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, IResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<IResponse<T>> {
return next.handle().pipe(
map((data) => ({
data,
code: 200,
msg: 'success',
success: true,
})),
);
}
}
然后在 src/main.ts 里面配置全局拦截器,来让我们的拦截器生效起来,具体代码如下:
javascript
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// 全局拦截器
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
全局异常处理
正常的接口返回参数的格式统一之后,我们还需要对异常接口的返回进行标准化的统一处理。
首先在 src/exceptions 目录下新建一个 base.exception.filter.ts 该过滤器是用来做兜底的统一错误处理,具体代码如下:
typescript
import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, ServiceUnavailableException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class BaseExceptionFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
data: null,
code: HttpStatus.SERVICE_UNAVAILABLE,
msg: new ServiceUnavailableException().getResponse(),
success: false,
});
}
}
然后再在 src/exceptions 目录下新建一个 http.exception.filter.rs 该过滤器是专门用来处理 http 相关的异常错误,具体代码如下:
php
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).send({
data: null,
code: status,
msg: exception.getResponse(),
// path: request.url,
success: false,
});
}
}
最后在 src/main.ts 里面配置全局过滤器,来让我们的过滤器生效起来,具体代码如下:
javascript
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// 全局异常过滤器
app.useGlobalFilters(new BaseExceptionFilter(), new HttpExceptionFilter());
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
注意,过滤器注册的先后顺序不要弄错了
环境变量配置
在项目的开发周期中,一般都会有 本地环境、测试环境、正式环境 之分,某些情况下 测试环境 可能还有多套,那么相对应的一些相关配置也需要适配不同的环境,这时候就需要用到环境配置了。在前端项目中我们可能用到最多的就是 dotenv 这一套了,实际上 nest.js 内置的也是 dotenv 这一套环境变量配置,但是这里推荐使用 YAML 来配置环境变量。
首先需要安装 yaml 的库,执行 pnpm add yaml
即可,然后在根目录下面新建 config
的目录,并根据环境创建对应的 yaml 文件,例如:.dev.yaml、.test.yaml、.prod.yaml 等。
然后在 utils 目录下面新建 index.ts、const.ts、env.config.ts 文件。
其中 index.ts 文件作为聚合导出用的(个人习惯不喜勿喷),如图所示:
const.ts 文件作为全局常量的定义位置,目前只有一个 export const ENV = process.env.RUN_ENV;
这里定义了 ENV 的常量,并且将 process.env.RUN_ENV 赋值给 ENV ,所以我们还需要在 package.json 文件中修改一下相关的执行命令,需要在前面添加一句 cross-env RUN_ENV=*(当前环境枚举值)
,具体如图所示:
env.config.ts 文件中就是读取和设置当前环境变量的实现了,根据 ENV 读取对应的 yaml 文件,然后使用 yaml 库的 parse 把该文件解析成 JSON 对象并返回,并且组装 ConfigModule 的 Options 配置,具体代码如下:
javascript
import * as fs from 'fs';
import * as path from 'path';
import { parse } from 'yaml';
import { ConfigModuleOptions } from '@nestjs/config';
import { ENV } from '.';
export const loadEnvConfig = () => {
const filePath = path.join(process.cwd(), `./config/.${ENV}.yaml`);
const file = fs.readFileSync(filePath, 'utf8');
return parse(file);
};
// isGlobal: true; 表示是全局模块
// ignoreEnvFile: true; 表示忽略 dotenv 的文件
// load: [loadEnvConfig]; 表示为装载的环境变量数据
export const configModuleOptions: ConfigModuleOptions = { isGlobal: true, ignoreEnvFile: true, load: [loadEnvConfig] };
最后还需要在 app.module.ts 文件中添加 ConfigModule 的配置即可,具体代码如下:
python
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { configModuleOptions } from './utils';
@Module({
imports: [ConfigModule.forRoot(configModuleOptions),],
controllers: [],
providers: [],
})
export class AppModule {}
接口日志
日志是后端服务中必不可少的一个内容,一旦出现问题需要定位和追踪就必然需要查询日志获取相关的信息,否则极难对问题进行跟进和处理。
nest.js 本身其实是携带了日志功能的,但是提供的能力比较基础不足以满足日常所需,我们这里选择使用 winston 库来实现日志功能来满足日常所需。
首先需要安装几个依赖:
sql
pnpm add winston dayjs chalk@4
然后在 src/logger 目录下新建一个 log.ts 的文件,大体就是实现 LoggerService 类型的基本功能,具体代码如下:
typescript
import { LoggerService } from '@nestjs/common';
import { createLogger, Logger, LoggerOptions } from 'winston';
import * as dayjs from 'dayjs';
export class Log implements LoggerService {
private logger: Logger;
constructor(options: LoggerOptions) {
this.logger = createLogger(options);
}
log(message: string, context: string) {
const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logger.log('info', message, { context, time });
}
error(message: string, context: string) {
const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logger.log('error', message, { context, time });
}
warn(message: string, context: string) {
const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logger.log('warn', message, { context, time });
}
debug(message: string, context: string) {
const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logger.log('debug', message, { context, time });
}
verbose(message: string, context: string) {
const time = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss');
this.logger.log('verbose', message, { context, time });
}
}
然后在 src/logger 目录下创建一个 winston.module.ts 的文件,这是一个动态模块,用来全局注入动态配置的日志服务的,具体代码如下:
java
import { DynamicModule, Global, Module } from '@nestjs/common';
import { LoggerOptions } from 'winston';
import { Log } from './log';
export const WINSTON_LOGGER_TOKEN = 'WINSTON_LOGGER';
@Global()
@Module({})
export class WinstonModule {
static forRoot(options: LoggerOptions): DynamicModule {
return {
module: WinstonModule,
providers: [{ provide: WINSTON_LOGGER_TOKEN, useValue: new Log(options) }],
exports: [WINSTON_LOGGER_TOKEN],
};
}
}
再然后还需要在 src/utils 目录下创建一个 logger.config.ts 的文件(记得在 index.ts 文件中导出),来配置 winston 的日志规则,大体上就是服务运行时在命令行中打印的日志格式,以及 错误日志 和 正常日志 落地为本地文件的相关配置,具体代码如下:
php
import { transports, format, LoggerOptions } from 'winston';
import * as chalk from 'chalk';
import 'winston-daily-rotate-file';
export const winstonModuleOptions: LoggerOptions = {
level: 'debug',
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ context, level, message, time }) => {
const contextStr = chalk.yellow(`[${context}]`);
return `${level} ${time} ${contextStr} ${message} `;
}),
),
}),
new transports.DailyRotateFile({
level: 'error',
filename: 'logs/%DATE%.error.log',
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '31d',
format: format.json(),
}),
new transports.DailyRotateFile({
level: 'info',
filename: 'logs/%DATE%.info.log',
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '31d',
format: format.json(),
}),
],
};
最后需要在 src/app.module.ts 中将日志模块注册,并且在 src/main.ts 里面设置该模块为日志模块,具体代码如下:
app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WinstonModule } from './logger/winston.module';
import { configModuleOptions, winstonModuleOptions } from './utils';
@Module({
imports: [
ConfigModule.forRoot(configModuleOptions),
WinstonModule.forRoot(winstonModuleOptions)
],
controllers: [],
providers: [],
})
export class AppModule {}
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// 全局日志
app.useLogger(app.get(WINSTON_LOGGER_TOKEN));
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
跨域设置
跨域是前端开发中老生常谈的问题了,在测试环境或者开发环境中常常就需要全开跨域,在正式环境中就需要谨慎开放跨域了,大多都是通过 nginx 做反向代理来处理跨域问题。
nset.js 开启跨域还是比较简单的,只需要在 src/mian.ts 里面配置 app.enableCors() 即可,不过一般情况下服务是大部分接口都不允许跨域,小部分开放性的接口需要跨域,所以还需要在 src/utils 目录下创建一个 cors.config.ts 文件来动态管理跨域,具体代码如下:
cors.config.ts
const white = []; // 跨域白名单
export const corsOptionsDelegate = (req: Request, callback) => {
const { url } = req;
if (white.find((item) => url.startsWith(item))) return callback(null, { origin: true });
callback(null, { origin: false });
};
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// CROS
// 快速全开跨域可以直接使用 app.enableCors() 即可;
app.enableCors(corsOptionsDelegate);
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
统一路由前缀
不知道 jym 接触过的是否是这样,但是我接触过的大多数接口都是在 api/ 这个路由下的,统一的路由前缀其实也更方便做 nginx 的反向代理以及一些统一性的处理,在 nest.js 中只需要通过 api.setGlobalPrefix(prefix) 就可以设置统一的路由前缀了。
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// 设置路由前缀
app.setGlobalPrefix('api/');
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
接口文档
相信这玩意儿大家都不陌生,是前后端对接的必备品之一,所以就不多说了。
首先我们需要安装相关的依赖 pnpm add @nestjs/swagger
, 其次 src/docs.ts 文件中封装一个创建文档实例的方法,具体也没什么好说的就先使用 api 创建文档的 options,然后调用 createDocument 的方法创建实例,最后用 setup 来设置文档的路由,具体代码如下:
javascript
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as pkg from '../package.json';
export const createInterfaceDocument = (app) => {
const options = new DocumentBuilder().setTitle(pkg.name).setDescription(pkg.description).setVersion(pkg.version).build();
const doc = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api/doc', app, doc);
};
然后就只需要在 main.ts 中执行 createInterfaceDocument 方法了:
scss
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// 创建接口文档
createInterfaceDocument(app);
await app.listen(3000);
console.log('http://localhost:3000');
}
bootstrap();
数据库连接
最后的最后我们还需要连接数据库这样我们的项目的基础框架就搭建完毕了,这里我们直接使用 typeorm 框架就可以了,该库极大程度上减少了对 SQL 语法的要求,降低了数据库的使用门槛,可以直接使用 api 的形式来和数据库做交互。
首先还是安装相关的依赖库 $ pnpm add @nestjs/typeorm mysql2
,然后在 src/utils 目录下新建 mysql.config.ts 的文件,该文件是创建动态模块 TypeOrmModule 的数据库相关配置,另外 synchronize 是自动同步表结构,初始化的时候以及 test 可以考虑为 true 但是正式环境请不要设置 true 容易导致数据库崩掉,具体代码如下:
javascript
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { loadEnvConfig } from '.';
const { MYSQL_CONFIG } = loadEnvConfig();
export const typeOrmModuleOptions: TypeOrmModuleOptions = {
type: 'mysql',
host: MYSQL_CONFIG.host,
port: MYSQL_CONFIG.port,
username: MYSQL_CONFIG.username,
password: MYSQL_CONFIG.password,
database: MYSQL_CONFIG.database,
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: MYSQL_CONFIG.synchronize,
};
总结
这节其实没什么实际上的内容,但是也是项目开发中必不可少的一件事,搭建项目框架,当然搭建一次后,后面就可以重复使用了,觉得太零散或或者内容太多也问题不大,可以直接 clone nest-template 这个库即可运行(当然数据库和数据配置你得改成你自己的哈)。