Nestjs 格式化接口响应的数据结构
前言
Nestjs中,如果你想要接口统一返回给前端的数据对象格式,需要从两方面去处理,第一是接口正常的情况下,我们需要通过拦截器去改写数据格式;第二是异常情况,比如数据校验不通过,代码报错之类的,我们需要通过异常过滤器来改写数据格式。
正常的做法就是实现一个全局的拦截器和过滤器。
响应数据对象格式
{
"code": 200,
"message": "OK",
"data": { }
}
code
,是后端自定义的错误码;message
,是后端的响应消息;data
,是具体的响应内容;
依赖
不同的状态码response.statusCode
其实是有W3C的官方预设定义的,但是Nestjs如果没有具体设置错误对象的message,那么通过响应对象response.statusMessage
得到的就是undefined,通过依赖http-status-codes
就可以传statusCode
得到一个英文版的预定义消息。
如果你要中文的,可以自己去定义一个map对象,自己将code码对象的消息翻译下,这样也是一样的。
pnpm i http-status-codes
实现一个全局异常过滤器
如果代码中发生了异常,报错了,都会进入到异常过滤器HttpException
,而不会进入到拦截器中了。
在之前的文章中我们实现了一个用于处理表单校验的过滤器,针对BadRequestException
的异常,我们将错误的message做了特殊处理,现在我们基于这个增强并修复一些问题。
首先通过cli命令我们创建一个过滤器文件:
nest g f utils/filters/httpException --no-spec
此时会生成一个http-exception.filter.ts
文件,在utils/filters/httpException 目录下。
然后填入以下内容:
import { BadRequestException, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
import type { ArgumentsHost } from "@nestjs/common";
import type { Response } from "express";
import { getReasonPhrase } from "http-status-codes";
import type { ValidateErrorMessage } from "@/common/types";
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException | Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let status = 500; // 默认http status状态错误码
let code = 500; // 默认后端自定义错误码
let message: string | ValidateErrorMessage = getReasonPhrase(code); // 默认错误信息
if (exception instanceof HttpException) {
// Nestjs预设的错误
status = exception.getStatus();
const results = exception.getResponse() as any;
code = results.statusCode;
message = results.message ?? getReasonPhrase(code);
// 参数校验错误,默认都是BadRequestException
const isArrayMessage = Array.isArray(results.message);
const isValidationError =
isArrayMessage && typeof results.message[0] === "string" && results.message[0].includes("⓿");
if (exception instanceof BadRequestException && isValidationError) {
const validateMessage: ValidateErrorMessage = [];
results.message.forEach((item) => {
const [key, val] = item.split("⓿") as [string, string];
const findData = validateMessage.find((item) => item.field === key);
if (findData) {
findData.message.push(val);
} else {
validateMessage.push({ field: key, message: [val] });
}
});
message = validateMessage;
}
} else {
// 其他错误
message = exception.message ?? getReasonPhrase(code);
}
return response.status(status).json({
code,
message,
data: null
});
}
}
类型文件:http-exception.type.ts
/** 校验错误消息类型 */
export type ValidateErrorMessage = Array<{
field: string;
message: Array<string>;
}>;
需要注意一点,catch的第一个参数是错误对象,在官方文档中,它一般会被指定为HttpException
类型,这个类型是Nestjs中所有预设错误的基类,但是这个参数不一定就是HttpException,也有可能是服务中代码出现的错误对象,这个错误对象就不在是预设的了,所以需要加上if判断一下。常见的一种情况就是利用@nestjs/jwt
的服务来手动解密token时,如果token不是一个合法的,会直接抛出一个错误,这个错误就不是Nestjs预设的了。
实现一个全局拦截器
nest g itc utils/interceptor/resultFormat --no-spec
通过cli命令生成一个result-format.interceptor.ts
文件,在utils/interceptor/resultFormat 目录下。
填入以下内容:
import { hasKeys } from "@/utils/tools";
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import type { Response } from "express";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { getReasonPhrase } from "http-status-codes";
@Injectable()
export class ResultFormatInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const response = ctx.getResponse<Response>();
const code = response.statusCode;
const message = response.statusMessage || getReasonPhrase(code);
return next.handle().pipe(
map((data) => {
// 判断是否已经是格式化的数据
if (data) {
const hasFormat = hasKeys(data, ["code", "message", "data"]);
if (hasFormat) return data;
}
return {
code: code,
message: message,
data: data ?? null
};
})
);
}
}
这里我们引入了一个小工具函数hasKeys
,他在tools.ts
里面这么写的:
// tools.ts
/** 判断对象上指定的key是否存在 */
export function hasKey<O extends object>(obj: O, key: keyof any): key is keyof O {
return Object.hasOwn(obj, key);
}
/** 判断对象上指定的多个key是否存在 */
export function hasKeys<O extends object>(obj: O, keys: Array<keyof any>): keys is Array<keyof O> {
return keys.every((key) => hasKey(obj, key));
}
我们使用es2020定义的新的函数Object.hasOwn
来判断对象中的属性是否存在,不检测原型链上的属性。
如果对象已经是格式化好的,就直接返回,反之则调整数据结构。
需要注意一点,当我们的控制器没有return任何内容的时候,直接调用hasKeys
会出现问题,毕竟undefined和null你怎么has判断啊,所以要加个if前置条件是data是有内容的时候。
然后就是如果data为undefined,就会导致返回给前端的对象变成如下:
{
"code": 201,
"message": "Created",
}
data属性不见了,原因是因为HTTP 协议本身并不支持直接返回 undefined
作为响应体。因此,当 NestJS 控制器没有返回值时,NestJS 的内部机制会处理这种情况,并决定发送一个不含内容的响应给客户端。
所以我们自己加个判定,如果是undefined或者null,就返回null,当然你也可以自己定义空字符串,也是可以的。
tsconfig.ts配置
由于Object.hasOwn
还是比较新的api,我们需要在tsconfig.ts
配置一下lib
属性:
{
"compilerOptions": {
"target": "ES2021",
"lib": ["es2022"],
}
}
这样就不会提示警告了。
注册全局过滤器和拦截器
我们可以在根模块app.module.ts
中,通过providers
进行注入,但是从语义上讲并不是很清晰,我们可以通过usexxx
的方式进行全局注入。
打开main.ts
:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { HttpExceptionFilter } from "@/utils/filters";
import { ResultFormatInterceptor } from "@/utils/interceptor";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResultFormatInterceptor());
await app.listen(3000);
}
bootstrap();
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据