返回格式化拦截器
在上一篇《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).req
return 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_ERROR
response.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 = code
this.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 = null
const 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)
}
重新运行项目,我们看到返回数据已经显示完整了: