NestJS博客实战05-统一的结果返回

763 阅读5分钟
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

hello,大家好! 有环境配置,有了Log日志,接下来做一做,统一返回正常异常结果的格式。本来这章内容准备晚一点写的,因为在这个主要以模版引擎来渲染页面的项目里面作用可能不是很大,但是之前有位兄弟在文章评论里面希望我能够说一说统一返回。所以这一章主要会介绍异常过滤器,并通过异常过滤器返回统一的异常结果,顺便用上一章的Logger日志把错误信息记录下来。正常的情况下,用拦截器返回一个统一格式个正常结果。

异常过滤器Filter

异常过滤器具体的内容可以参照这篇文章异常过滤器还有小技巧怎样抛出异常才能更专业

了解过滤器了以后,我们先来实现一个基本异常过滤。先通过命令创建一个filter

1. 基本异常过滤器配置

nest g f common/exceptions --no-spec

把文件名字改成base.exception.filter.ts里面的内容如下,别忘了我们使用的是fastify

import { FastifyReply, FastifyRequest } from 'fastify';

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpStatus,
  ServiceUnavailableException,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();

    // 暂时Console出来
    console.log(exception);

    const exResponse = new ServiceUnavailableException().getResponse();
    if (typeof exResponse === 'string') {
      // 非 HTTP 标准异常的处理。
      response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
        statusCode: HttpStatus.SERVICE_UNAVAILABLE,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: new ServiceUnavailableException().getResponse(),
      });
    } else {
      // 非 HTTP 标准异常的处理。
      response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
        statusCode: HttpStatus.SERVICE_UNAVAILABLE,
        timestamp: new Date().toISOString(),
        path: request.url,
        ...(new ServiceUnavailableException().getResponse() as any),
      });
    }
  }
}

然后在main.ts引入这个异常过滤器

async function bootstrap() {
  const fastifyOption: any = {};

  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(fastifyOption),
  );

  // 异常过滤器
  app.useGlobalFilters(new AllExceptionsFilter());

  await app.listen(SERVER_VALUE.port, SERVER_VALUE.host);
}
bootstrap();

这样引入以后,全局的异常过滤器就生效了,但是有个问题,在这里直接通过new来创建的情况下,Nest内置的依赖注入就不能用了。但是我想在异常处理里面调用LoggerService,如何能够注入这个服务呢?

2. 升级基本异常过滤器配置

官网的自定义provider文档,写了如果想用Nest的依赖注入,就需要在app.module.tsproviders导入。

所以删掉main.ts中的过滤器代码,在app.module.ts

import { APP_FILTER } from '@nestjs/core';

@Global()
@Module({
  providers: [
    {
      provide: APP_FILTER, 
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

接着回到base.exception.filter.ts中注入LoggerService

import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  // 依赖注入
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly Logger: LoggerService,
  ) {}

  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();

    // 大家根据自己项目情况写,我这里只是做个示范 
    this.Logger.error('exception:' + JSON.stringify(exception));
    this.Logger.error('url:' + JSON.stringify(request.url));
    this.Logger.error('body:' + JSON.stringify(request.body));
    this.Logger.error('query:' + JSON.stringify(request.query));
    this.Logger.error('params:' + JSON.stringify(request.params));
    this.Logger.error('headers:' + JSON.stringify(request.headers));

   ...
}

3. 业务异常过滤器配置

基本异常过滤主要是处理我们不能预判的异常,如果能有预判到异常的情况(例如:没有接口请求权限等等),一般这种异常应该要给一个正常状态(status:200)。但是要传message消息,告诉前端出现了业务问题。所以我们就需要配置自己的业务异常。详细内容可以参照官网的自定义异常,主要思路是继承Nest内置的异常,并进行扩展。

common/exceptions文件夹里面创建business.exception.ts,然后写代码如下:

import { HttpException, HttpStatus } from '@nestjs/common';
import { BUSINESS_ERROR_CODE } from './business.error.codes';

type BusinessError = {
  code: number;
  message: string;
};

export class BusinessException extends HttpException {
  constructor(err: BusinessError | string) {
    if (typeof err === 'string') {
      err = {
        code: BUSINESS_ERROR_CODE.COMMON,
        message: err,
      };
    }
    super(err, HttpStatus.OK);
  }

  // 举例:自定义无权限异常(大家根据自己的业务情况追加)
  static throwForbidden() {
    throw new BusinessException({
      code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
      message: '抱歉哦,您无此权限!',
    });
  }
}

假设几种异常的情况,并赋予异常的编号。

export const BUSINESS_ERROR_CODE = {
  // 公共错误码
  COMMON: 10001,
  // 特殊错误码
  TOKEN_INVALID: 10002,
  // 禁止访问
  ACCESS_FORBIDDEN: 10003,
  // 权限已禁用
  PERMISSION_DISABLED: 10003,
  // 用户已冻结
  USER_DISABLED: 10004,
};

有了自己的业务异常,我们要有一个专门的过滤器来处理这个异常

nest g f common/exceptions --no-spec

把名字改成http.exception.filter.ts,因为我们的业务异常继承自HttpException,所以我们用Catch(HttpException)来捕获所有的HttpException异常。然后判断异常的类型,如果是BusinessException我们就单独处理,返回正常的状态,并返回异常消息内容。如果不是则和base.exception.filter.ts的处理方法一样。

import { BusinessException } from './business.exception';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly Logger: LoggerService,
  ) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<FastifyReply>();
    const request = ctx.getRequest<FastifyRequest>();
    const status = exception.getStatus();

    this.Logger.error('exception:' + JSON.stringify(exception));
    this.Logger.error('url:' + JSON.stringify(request.url));
    this.Logger.error('body:' + JSON.stringify(request.body));
    this.Logger.error('query:' + JSON.stringify(request.query));
    this.Logger.error('params:' + JSON.stringify(request.params));
    this.Logger.error('headers:' + JSON.stringify(request.headers));

    // 处理业务异常
    if (exception instanceof BusinessException) {
      const error = exception.getResponse();
      response.status(HttpStatus.OK).send({
        response: null,
        responseTime: new Date(),
        status: error['code'],
        message: error['message'],
        success: false,
      });
      return;
    }

    const exResponse = exception.getResponse();

    if (typeof exResponse === 'string') {
      response.status(status).send({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        message: exception.getResponse(),
      });
    } else {
      response.status(status).send({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        ...(exception.getResponse() as any),
      });
    }
  }
}

别忘了在app.module.ts注册。

拦截器interceptors

讲完了,异常的过滤,那么正常的情况我们怎么返回一个统一格式的消息呢。这就要提到另一个Nest的概念拦截器interceptors,拦截器具体内容我就不重复写了。

nest g itc common/interceptors --no-spec

拦截器代码如下

interface Response<T> {
  response: T;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      map((data) => ({
        // 告诉前端这不是一个异常
        success: true,
        cmd: request.url,
        response: data,
        responseTime: new Date(),
      })),
    );
  }
}

然后在main.ts中加入拦截器

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  
  // 统一响应体格式
  app.useGlobalInterceptors(new TransformInterceptor());

  //....
  await app.listen(serverValue.port, serverValue.host);
}
bootstrap();

结语

在这一篇文章中,介绍了怎么用异常过滤器,拦截器来统一返回前端的格式。如果大家觉得这篇文章对您有帮助,别忘了点赞/评论。

预告:下一章开始介绍模版引擎。

本章代码

代码