前言

Nestjs中,如果你想要接口统一返回给前端的数据对象格式,需要从两方面去处理,第一是接口正常的情况下,我们需要通过拦截器去改写数据格式;第二是异常情况,比如数据校验不通过,代码报错之类的,我们需要通过异常过滤器来改写数据格式。

正常的做法就是实现一个全局的拦截器和过滤器。

响应数据对象格式

{
    "code": 200,
    "message": "OK",
    "data": { }
}
  1. code,是后端自定义的错误码;
  2. message,是后端的响应消息;
  3. 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();
分类: Nest.js 标签: 接口全局过滤器格式化Nestjs数据结构全局拦截器

评论

暂无评论数据

暂无评论数据

目录