Nest世界中的AOP

对于前端开发来说,Nest中有很多全新的概念,这里我们来学习一下nest中AOP思想,并且学习Nest中提供的AOP方式。

AOP

Nest 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Interceptor、ExceptionFilter。他们其实都是中间件的概念,只是分成了具体的内容,不同中间件做不同事情。

这张图很形象的介绍了nest整个运行周期所经过的核心逻辑流程。

中间件 Middleware

默认情况下由于nest底层是express提供服务,所以nest中间件就是express中的中间件。在nest中中间件有两种。一般用于处理请求和响应的,在nest提供的AOP服务中,请求最先到达的就是中间件处理层。

  • 全局中间件,他作用于nest任何一个controller方法。
js 复制代码
// main.ts
app.use((req, res, next) => {
    console.log('global middleware before', req.url);
    next();
    console.log('global middleware after');
});
  • 路由和控制器中间件。 中间件的定义就是继承自NestMiddleware接口,并实现use方法。我们一般在app.module.ts中进行使用。
js 复制代码
 export class AppModule implements NestModule {
  // 使用中间件
  configure(consumer: MiddlewareConsumer) {
    /**
     * apply可以指定多个中间件函数或者类
     * exclude 可以设置规则匹配排出的路由,一般用于设置控制器中间件。
     */
    // 1.可以设置路由中间件(匹配规则,限定方法)
    // consumer.apply(LoggerMiddleware).forRoutes({
    //   path: 'geturl*',
    //   method: RequestMethod.GET,
    // });
    // 1.可以设置特定路由
    // consumer.apply(LoggerMiddleware).forRoutes('api-test/geturlparams/1');
    // 2.设置控制器中间件(作用域当前控制器所有路由)
    consumer.apply(LoggerMiddleware, fnMiddleWare).forRoutes(ApiTestController);
  }
}

守卫 Guard

守卫是一个用 @Injectable() 装饰器的类,它实现了 CanActivate 接口。通过@UseGuards()使用。在所有中间件之后、任何拦截器或管道之前执行。他相较于middleware而言可以拿到程序执行的上下文。

  • 全局守卫,nest提供了两种注册全局守卫的方式。我们来看看区别

    • 直接在main.ts中进行注册。这种方式需要我们自己手动实例化,并不能交给nest的ioc容器。这样就导致我们不能在LoginGuard中进行注入其他依赖。
    js 复制代码
     app.useGlobalGuards(new LoginGuard());
    • app.module.ts中的provides进行注入。这种方式就是是让LoginGuard类交给nest去实例化和创建,并且可以注入其他依赖。
    js 复制代码
      providers: [
        {
          provide: APP_GUARD,
          useClass: LoginGuard,
        },
      ],
  • 控制器和路由守卫 这种守卫,直接使用UseGuards装饰器在对应的controller方法使用即可。

js 复制代码
@UseGuards(LoginGuard)
getUrlParam(@Param('id') id: string) {
    console.log('获取get请求url param参数');
    return `获取get请求url param参数: ${id}`;
}

拦截器 Interceptor

拦截器是用 @Injectable() 装饰器的类并实现 NestInterceptor 接口。他和guard一样都可以拿到程序执行的上下文。并且和Guard的作用级别一样,有全局,控制器,路由级别。他区别于其他AOP,提供了前置和后置拦截器,可以在路由处理程序前后做一些逻辑处理。例如设置当前项目的接口标准返回值格式。

js 复制代码
// 拦截器
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LogInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // context和guard传入的对象一样,可以获取当前执行上下文,next内部提供了一个handle方法,用于调用控制器方法
    console.log('interceptor before...');
    return next.handle().pipe(tap(() => console.log(`interceptor after...`)));
  }
}
  • 全局拦截器,也有两种方式,区分的作用和Guard一样。
js 复制代码
// 方式一 main.ts
app.useGlobalInterceptors(new LogInterceptor());

// 方式二 app.module.ts
providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LogInterceptor,
    },
]
  • 控制器和路由 这种拦截器,直接使用UseInterceptors装饰器在对应的controller方法使用即可。
js 复制代码
  @UseInterceptors(LogInterceptor)
  getUrlParam(@Param('id') id: string) {
    console.log('获取get请求url param参数');
    return `获取get请求url param参数: ${id}`;
  }

了解了拦截器的用法后,我们就来看看如何实现企业级项目的接口的返回值格式。

js 复制代码
// 接口一般都会返回和谐信息
{
    data, // 数据
    status: 0, // 接口状态值
    extra: {}, // 拓展信息
    message: 'success', // 异常信息
    success:true // 接口业务返回状态
}

实现,就是拿到路由处理程序返回值做数据映射。

js 复制代码
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface Response<T> {
  data: T;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    // 拿到路由处理程序返回值做数据映射
    return next.handle().pipe(
      map((data) => ({
        data,
        status: 0,
        extra: {},
        message: 'success',
        success: true,
      })),
    );
  }
}

管道 pipe

管道是用 @Injectable() 装饰器的类,它实现了 PipeTransform 接口。

管道有两个典型的用例:

  • 转型:将输入数据转换为所需的形式(例如,从字符串到整数)。
  • 验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常。

管道和其他AOP模型一样,都可以作用于路由,路由控制器和全局,并且管道还可以作用于路由处理器参数

js 复制代码
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';

@Injectable()
export class TestPipe implements PipeTransform {
  // 接受原始数据和方法参数的元素据
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value !== 'number') {
      throw new BadRequestException('url param不是数字');
    }
    return value * 10;
  }
}
  • 全局管道,也有两种方式,区分的作用和Guard一样。
js 复制代码
 // main.ts
  app.useGlobalPipes(new TestPipe());
  
 // app.module.ts
  providers: [
    {
      provide: APP_PIPE,
      useClass: TestPipe,
    },
  ],
  • 控制器,路由,路由处理函数参数管道

直接使用UsePipes装饰器在对应的controller方法和参数解析器(Param, Query, Body等等)中使用即可。

Nest内置了九个开箱即用的管道,除了ParseFilePipe外转化错误会抛出BadRequestException异常,ParseFilePipe中大小错误将抛出PayloadTooLargeException异常,类型错误将抛出UnsupportedMediaTypeException异常。

ValidationPipe

基于类验证器(class - validator)库,对传入的请求数据(通常是 DTO - Data Transfer Object 中的数据)进行全面验证。可以验证数据的格式、类型、范围等各种规则。

ParseIntPipe

专门用于将输入的值转换为整数类型。在处理需要整数类型数据的场景下非常有用,如处理路由参数、查询参数中的数字标识。

ParseFloatPipe

与 ParseIntPipe 类似,主要用于将输入的值转换为浮点数类型。适用于涉及金额、比例、坐标等需要浮点数表示的数据场景。

ParseBoolPipe

用于将输入的值转换为布尔类型。可以处理来自请求中的各种形式表示的布尔值,如字符串形式的 'true'、'false' 或者数字形式的 1、0 等。

ParseArrayPipe

能够将输入的数据解析为数组形式。可用于处理以特定格式传递的数组数据,如逗号分隔的字符串转换为数组,或者解析复杂格式的数组数据。

ParseUUIDPipe

专门用于解析通用唯一识别码(UUID)类型的数据。在处理与唯一标识相关的场景,如数据库主键为 UUID 类型或者处理外部系统传递的 UUID 标识时非常有用。

ParseEnumPipe

用于将输入的值转换为对应的枚举类型。在 NestJS 中,当业务逻辑使用枚举类型来表示特定的状态或类型时,此管道可确保传入的数据与定义的枚举类型相匹配。

DefaultValuePipe

当传入的参数可能为空或者未定义(null, undefined)时,为该参数设置一个默认值。这在处理可选参数或者数据可能缺失的情况下非常有用。

ParseFilePipe

用于处理文件上传相关的操作。它可以对上传文件的大小、类型等进行限制和验证,确保上传的文件符合应用的要求。

异常过滤器 ExceptionFilter

Nest 带有一个内置的异常层,负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。

Nest 提供了一组继承自基 HttpException 的标准异常。

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

内置的异常处理层已经可以满足处理异常了,但是你可能希望根据某些动态因素添加日志记录或使用不同的 JSON 模式。异常过滤器正是为此目的而设计的。它们让你可以控制准确的控制流和发送回客户端的响应内容。

异常过滤器都需要实现ExceptionFilter接口。使用@Catch()修饰类,并且指定要处理的异常类型,如果未传递异常类型,那么该过滤器将捕获所有类型的异常。 这对于一个健壮的程序来说非常重要,返回统一且明确的信息,可以很快定位错误。

下面我们来看看如何来实现企业级程序的异常处理器。

我们需要处理程序中抛出的所有异常,所以我们就需要实现一个捕获全部异常的过滤器。

js 复制代码
// base.exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpStatus,
  ServiceUnavailableException,
  LoggerService,
  Inject,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(
    // 该模块需要等到logger初始化完成之后在进行初始化。
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private logger: LoggerService, // 可以注入日志系统
  ) {}
  catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse<Response>(); // 获取响应对象
    const request = ctx.getRequest<Request>(); // 获取请求对象

    this.logger.error(exception); // 输出错误日志

    // 非 HTTP 标准异常的处理。
    response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
      statusCode: HttpStatus.SERVICE_UNAVAILABLE,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: new ServiceUnavailableException().getResponse(),
    });
  }
}

如果程序抛出HttpException相关异常,我们就需要设置HttpException统一的异常过滤器,返回更加信息的错误内容。

js 复制代码
// http.exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { BusinessException } from './business.exception';
import { Request, Response } from 'express';

@Catch(HttpException) // 指定HttpException,只捕获所有http相关的异常
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(); // 获取当前错误的状态码。由于所有http异常都继承自HttpException,所以这里可以拿到

    // 处理业务异常(主动抛出的异常)
    if (exception instanceof BusinessException) {
      const error = exception.getResponse(); // BusinessException 异常内部提供error, message属性
      response.status(HttpStatus.OK).send({
        data: null,
        status: error['code'],
        extra: {},
        message: error['message'],
        success: false,
      });
      return;
    }

    response.status(status).send({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.getResponse(),
    });
  }
}

还有业务相关主动抛出异常我们也可以设置一种过滤器来进行处理。

js 复制代码
// 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: '抱歉哦,您暂无权限!',
    });
  }
}

// business.error.codes.ts
export const BUSINESS_ERROR_CODE = {
  // 公共错误码
  COMMON: 10001,

  // 特殊错误码
  TOKEN_INVALID: 10002,

  // 禁止访问
  ACCESS_FORBIDDEN: 10003,

  // 权限已禁用
  PERMISSION_DISABLED: 10003,

  // 用户已冻结
  USER_DISABLED: 10004,
};

这样我们项目中的异常处理就很健壮了,下面我们来测试一下相关功能。

调试

浏览器中调试(npm run start:debug)

对于前端开发者来说,可能更习惯于在浏览器中进行调试。nest也方便的提供了调试运行命令。npm run start:debug。然后会启动一个ws服务,我们就可以在内部进行断点调试了。

vscode中调试(npm run start:dev)

可是一般开发后台服务并不需要关心页面内容,所以可能在编译器中调试会更合适,所以我们可以直接使用vscode提供的调试服务。

创建一个launch.json配置文件。然后通过debug运行项目。

js 复制代码
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "debug nest",
      "runtimeExecutable": "npm", // 运行命令
      "args": [ // 命令参数
          "run",
          "start:dev",
      ],
      "skipFiles": [
          "<node_internals>/**"
      ],
      "console": "integratedTerminal", // 用 vscode 的内置终端来打印日志
    }
  ]
}

或者一个更简单的方式,ctrl + shift + p搜索Toggle Auto Attach

然后启动运行命令调试程序即可。

提供器

提供器有两种方式注入到其他提供器和控制器中。

  • 基于构造函数
js 复制代码
constructor(private readonly apiTestService: ApiTestService) {}
  • 基于属性
js 复制代码
@Inject()
private readonly ApiTestService: ApiTestService;

提供器可以由多种方式构成,并且提供器的token可以是类和字符串构成,如果是字符串构成,我们在@Inject时需要制定该字符串

  • 基于@Injectable标识的类,使用useClass引用。或者直接写类。
js 复制代码
providers: [
    ProviderTestService,
    // ProviderTestService 等价于
    {
      provide: ProviderTestService,
      useClass: ProviderTestService,
    },
],
  • 基于任意值,使用useValue引用。
js 复制代码
{
  provide: 'CUSTOM_VALUE',
  useValue: 'CUSTOM_VALUE',
},
  • 基于工厂函数,使用useFactory引用。这个可以注入其他提供器供其使用。并且支持异步。
js 复制代码
{
  provide: 'CUSTOM_FN',
  async useFactory() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('00000s');
      });
    });
  },
},
  • 如果想要修改提供器的token,我们还可以使用useExisting设置别名。为已存在的provide设置别名。
js 复制代码
  providers: [
    ProviderTestService,
    // ProviderTestService 等价于
    {
      provide: ProviderTestService,
      useClass: ProviderTestService,
    },
    {
      provide: 'CUSTOM_VALUE',
      useValue: 'CUSTOM_VALUE',
    },
    {
      provide: 'CUSTOM_FN',
      async useFactory() {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve('00000s');
          });
        });
      },
    },
    // 修改ProviderTestService提供器的名称,但是之前的名称还是可以使用的
    {
      provide: 'aliasProvide',
      useExisting: ProviderTestService,
    },
  ],

往期年度总结

往期文章

专栏文章

🔥如果此文对你有帮助的话,欢迎💗关注 、👍点赞 、⭐收藏✍️评论, 支持一下博主~

公众号:全栈追逐者,不定期的更新内容,关注不错过哦!

相关推荐
幼儿园老大*1 分钟前
【Echarts】折线图和柱状图如何从后端动态获取数据?
前端·javascript·vue.js·经验分享·后端·echarts·数据可视化
黄宏哲11 分钟前
自定义 CSS 和 t-att-class 的使用
前端·css·odoo
地球空间-技术小鱼23 分钟前
SQL常用语法
java·开发语言·前端
林中白虎27 分钟前
CSS实现服务卡片
前端·css
盼兮*36 分钟前
栏目一:使用echarts绘制简单图形
前端·信息可视化·echarts
BIGSHU09231 小时前
Spring Web是个什么东西
java·前端·spring
一张假钞1 小时前
Linux下Nodejs应用service配置
linux·运维·服务器·node.js
qbbmnnnnnn1 小时前
【前端开发入门】css快速入门
前端·css·css基础·css教程·css入门
xgq2 小时前
使用Credential Management API实现更安全的用户身份验证
前端·javascript·面试
bobostudio19952 小时前
TypeScript 算法手册【快速排序】
前端·javascript·算法·typescript