返回格式化拦截器
在上一篇《Nest.js权限管理系统开发(四)Swagger API接入》中,我们在base.controller.ts中创建了多个接口,每个接口都有不同的返回类型。现实中我们往往需要统一返回数据的格式,例如:
{"code": 200,"msg": "ok","data": "This action updates a #admin user"
}
next.js中我们可以通过返回格式拦截器对请求成功(状态码为 2xx)的数据进行一个格式化,同样的先执行
nest g interceptor common/interceptor/transform
创建一个拦截器,按照官网示例给的复制过来
import { CallHandler, ExecutionContext, NestInterceptor, Injectable } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { ResultData } from 'src/common/utils/result'@Injectable()
export class TransformInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {const req = context.getArgByIndex(1).reqreturn next.handle().pipe(map((data) => {return ResultData.ok(data)}),)}
}
在main.ts
中注册
import { TransformInterceptor } from './common/interceptor/transform/transform.interceptor';app.useGlobalInterceptors(new TransformInterceptor());
返回异常过滤器
自定义HttpException
这样做之后我们会发现请求成功的 code 只能是 200,一般项目中请求成功还需要很多业务异常状态码返回给前端,所以我们需要新建一个抛出业务异常的类ApiException
我们先创建common/enums/code.enum.ts
用于存放我们的业务状态码,这里简单写几个:
export enum ApiErrorCode {/** 公共错误 *//** 服务器出错 */SERVICE_ERROR = 500500,/** 数据为空 */DATA_IS_EMPTY = 100001,/** 参数有误 */PARAM_INVALID = 100002,
}
在common/filter/http-exception下新建api.exception.ts,
创建一个ApiException
类继承HttpException
,接受三个参数错误信息
,错误码code
,http状态码(默认是200)
import { HttpException, HttpStatus } from '@nestjs/common';
import { ApiErrorCode } from '../../enum/code.enum';export class ApiException extends HttpException {private errorMessage: string;private errorCode: ApiErrorCode;constructor(errorMessage: string,errorCode: ApiErrorCode,statusCode: HttpStatus = HttpStatus.OK,) {super(errorMessage, statusCode);this.errorMessage = errorMessage;this.errorCode = errorCode;}getErrorCode(): ApiErrorCode {return this.errorCode;}getErrorMessage(): string {return this.errorMessage;}
}
然后我们可以在需要的地方抛出相应的异常了。
异常过滤器
抛出异常不是异常请求的最终归宿。当我们使用 NestJS 内置的异常处理HttpException
,比如
throw new HttpException('您无权登录', HttpStatus.FORBIDDEN);
或者上面我们创建的ApiException,客户端就会收到
{"statusCode": 403,"message": "您无权登录"
}
但是这样不够灵活,所以我们可以新建一个异常过滤器进行自定义的操作:
nest g filter common/filter/http-exception
然后修改common/filter/http-exception/http-exception.filter.ts:
import {ExceptionFilter,Catch,ArgumentsHost,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();if (exception instanceof ApiException) {response.status(status).json({code: exception.getErrorCode(),msg: exception.getErrorMessage(),});return;}response.status(status).json({code: status,timestamp: new Date().toISOString(),path: request.url,msg: exception.message,});}
}
最后在main.ts
中进行注册
import { HttpExceptionFilter } from './common/filter/http-exception/http-exception.filter';app.useGlobalFilters(new HttpExceptionFilter());
除了HttpException,我们也要过滤普通异常:
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'@Catch()
export class ExceptionsFilter implements ExceptionFilter {catch(exception: any, host: ArgumentsHost) {const ctx = host.switchToHttp()const response = ctx.getResponse()const request = ctx.getRequest()const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERRORresponse.status(status).json({code: status,msg: `Service Error: ${exception}`,})}
}
Swagger
返回类型修复
前面我们已经对返回正常数据进行格式化,并拦截处理了异常发生时的返回数据格式。因为我们通过拦截器对返回数据进行了包裹,那么在我们的接口里,我们只需要返回data部分即可,不需要创建并返回各种Response类了。但是这里有个问题,由于 TypeScript 不存储有关泛型或接口的元数据,因此当你在 DTO 中使用它们时,SwaggerModule
可能无法在运行时正确生成模型定义。所以我们前面并没有采用类似下面的类作为我们的返回类型:
import { ApiProperty } from '@nestjs/swagger'export class ResultData<T> {constructor(code = 200, msg?: string, data?: T) {this.code = codethis.msg = msg || 'ok'this.data = data || undefined}@ApiProperty({ type: 'number', default: 200 })code: number@ApiProperty({ type: 'string', default: 'ok' })msg?: string@ApiProperty()data?: T
}
回到我们的例子中,要在不创建LoginResponse,只创建它的data的类型的情况下,如何实现等同于下面效果的Swagger注解:
@ApiOkResponse({ description: '登录成功返回', type: LoginResponse })
我们需要自定义一个装饰器,创建common/decorators/result.decorator.ts:
import { Type, applyDecorators } from '@nestjs/common'
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger'
import { ResultData } from '../utils/result'const baseTypeNames = ['String', 'Number', 'Boolean']
/*** 封装 swagger 返回统一结构* 支持复杂类型 { code, msg, data }* @param model 返回的 data 的数据类型* @param isArray data 是否是数组* @param isPager 设置为 true, 则 data 类型为 { list, total } , false data 类型是纯数组*/
export const ApiResult = <TModel extends Type<any>>(model?: TModel, isArray?: boolean, isPager?: boolean) => {let items = nullconst modelIsBaseType = model && baseTypeNames.includes(model.name)if (modelIsBaseType) {items = { type: model.name.toLocaleLowerCase() }} else if(model) {items = { $ref: getSchemaPath(model) }}let prop = {}if (isArray && isPager) {prop = {type: 'object',properties: {list: {type: 'array',items,},total: {type: 'number',default: 0,},},}} else if (isArray) {prop = {type: 'array',items,}} else if (items) {prop = items} else {prop = { type: 'null', default: null }}return applyDecorators(ApiExtraModels(...(model && !modelIsBaseType ? [ResultData, model] : [ResultData])),ApiOkResponse({schema: {allOf: [{ $ref: getSchemaPath(ResultData) },{properties: {data: prop,},},],},}),)
}
然后将ApiResult装饰器应用到接口方法上:
@Post('login')@ApiOperation({ summary: '登录' })@ApiResult(CreateTokenDto)async login(@Body() dto: LoginUser): Promise<CreateTokenDto> {return await this.userService.login(dto.account, dto.password)}
重新运行项目,我们看到返回数据已经显示完整了: