NestJS应用从0到1】3.优化整体异常,日志,请求拦截

1,453 阅读5分钟

简介:拦截器 日志 异常捕获

在前面两篇内容中已经对Nest的基础,日志,统一拦截,请求拦截有了一定的了解。但是如果需要将整个应用搭起来,我们需要将三者进行优化,以便后续开发

在这之前需要了解

我们仍旧需要对Nest所涉及到的几个基础知识进行一定的了解,当然我仍然推荐阅读官方文档得到更全面的了解。

在 NestJS 中,filterguardinterceptor 是三种不同的功能机制,它们各自有不同的用途和应用场景。每种机制都有其特定的应用场景,通过合理地使用它们,可以更好地管理和控制应用程序的行为。

  • Filter(过滤器):用于异常处理,捕获并处理应用程序中的未处理异常。
  • Guard(守卫):用于认证和授权,决定某个请求是否可以被处理。
  • Interceptor(拦截器):用于在请求处理过程中进行额外的逻辑处理,修改请求或响应、添加日志、进行性能监控等。

以下是它们的具体区别和用途:

1. Filter(过滤器)官方文档传送门

就是用来捕获异常,然后对异常进行一系列处理(如日志,统一错误返回),然后返回一个Http的Response请求

用途: 主要用于处理异常(错误处理)。过滤器可以捕获应用程序中的未处理异常,并将它们转换为适当的 HTTP 响应

工作方式: 当应用程序抛出异常时,过滤器会捕获该异常,并根据异常类型生成相应的 HTTP 响应。 过滤器可以是全局的,也可以是局部的(应用于特定的控制器或路由)。

示例:

/*这个示例是以express作为Http服务器,如果你使用的Fastify,你可根据Fastify的文档进行调整*/
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, 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;

/* 这里是Express 示例*/
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
    
    /* 下方为Fastify 示例, 其实区别不大*/
    response.code(status).send({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

2. Guard(守卫)官方文档传送门

用途: 主要用于认证和授权。守卫可以决定某个请求是否可以被处理(即是否允许访问特定的路由)。

工作方式: 守卫在请求到达控制器之前执行,可以基于请求中的信息(如 JWT 令牌、用户角色等)来决定是否允许访问。 守卫可以是全局的,也可以是局部的(应用于特定的控制器或路由)。

示例:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.some(role => user.roles?.includes(role));
  }
}

3. Interceptor(拦截器)官方文档传送门

用途: 主要用于在请求处理过程中进行额外的逻辑处理。拦截器可以在请求处理的各个阶段(如请求前、请求后)执行逻辑。

工作方式: 拦截器可以在请求到达控制器之前、请求处理过程中以及响应发送之前执行。它们可以修改请求或响应、添加日志、进行性能监控等。 拦截器可以是全局的,也可以是局部的(应用于特定的控制器或路由)。

示例:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 这里返回的pipe(管道),如果你有python种使用过scrapy 你对这个一定再熟悉不过了
    return next.handle().pipe(map(data => ({ data })));
  }
}

Code it

理清所有逻辑后,就很明确了各个模块的职能

在我实际开发中,我是这样使用的

  • 使用Guard(auth.guard.ts)进行用户身份验证,可参考之前文章
  • 使用另一个Interceptor(request-logging.interceptor.ts) 进行请求日志的记录
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Logger } from 'winston';
import { Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class RequestLoggingInterceptor implements NestInterceptor {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;
    const now = Date.now();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const statusCode = response.statusCode;
        const delay = Date.now() - now;

        this.logger.info(`${method} ${url} ${statusCode} - ${delay}ms`, {
          context: 'HTTP',
        });
      }),
    );
  }
}

  • 使用Interceptor(response.interceptor.ts)进行正常响应的处理,在异常响应时将异常抛出来给 error.filter.ts 进行处理
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { IResponseData } from './index';

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
  intercept(
    context: ExecutionContext,
    next: CallHandler<T>,
  ): Observable<IResponseData> {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        code: 200,
        data,
      })),
      catchError((error) => {
        // 直接抛出异常,用error.filter处理
        throw error;
      }),
    );
  }
}

  • 使用Filter(error.filter.ts) 处理所有的异常并调用logging记录日志,并统一返回错误内容

为了让所有接口都保持正常的Http200响应,我在处理错误时候,也会将HttpStatus改成200. 所以即便是后端报错,也会在Http请求上仍然是一个成功的请求。而在请求体中增加code和success来具体定义当前请求是否成功,且不成功的code,以便前端进行自定义处理

import { IResponseData } from '../response';
@Catch()
export class ErrorLoggingFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
  ) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();

     // 统一返回格式为 IResponseData
    const responseData: IResponseData = {
      code: ErrorCode.SERVER_ERROR,
      message: exception,
      success: false,
    };
    let status = HttpStatus.INTERNAL_SERVER_ERROR;

    if (exception instanceof CustomException) {
      status = exception.getStatus();
      responseData.message = exception.message;
      responseData.code = exception.code;
      responseData.data = exception.data;
    } else if (exception instanceof HttpException) {
      responseData.message = exception.message || exception.getResponse();
    } else if (exception instanceof Error) {
      responseData.message = exception.message;
    }
    const request = ctx.getRequest();
    this.logger.error(
      `${request.method} ${request.url} ${status} - ${JSON.stringify(responseData.message)}`,
      { context: 'Exception' },
    );

    // 强制将所有错误都使用200返回
    response.code(200).send(responseData);
  }
}

统一的返回格式定义:

export interface IResponseData {
  success: boolean; // 是否成功
  code: number; // 一个和前端对齐的code
  data?: any; // 返回的数据
  message?: string; // 当错误的时候会抛出消息
}

完结撒花 ❀❀❀❀❀❀