首页 > 其他分享 >Nestjs系列 Nestjs中的AOP架构

Nestjs系列 Nestjs中的AOP架构

时间:2024-03-04 15:33:29浏览次数:29  
标签:架构 app 中间件 num Nestjs context AOP AppModule 全局

什么是 AOP

Springboot 中就存在 AOP 切面编程。在 Nest 中也同样提供了该能力。
通常,一个请求过来,可能会经过 Controller(控制器)、Service(服务)、DataBase(数据库访问)的逻辑。
在这个流程中,若想要添加一些通用的逻辑,比如 日志记录、权限控制、异常处理等作为一个通用的逻辑。
AOP 的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,以方便项目的迭代与维护


Nest 中的 AOP 有五种:Middleware中间件、Guard守卫、Pipe管道、Interceptor拦截器、ExceptionFilter异常过滤器。

Middleware

Nest 中文网-中间件

全局中间件

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(function (req: Request, res: Response, next: NextFunction) {
    console.log('before-----', req.url);
    next();
    console.log('after-----');
  });

  await app.listen(3000);
}

当访问 http://localhost:3000/person 时,则会触发打印,同时,访问其它请求地址同样会触发该中间件逻辑,这就是全局中间件,可以在多个 handler 之间复用中间件的逻辑。

image

image

路由中间件

Nest 中除了全局中间件,还有路由中间件。
创建一个路由中间件

# --no-spec 不生成测试文件,--flat 平铺,不生成目录
nest g middleware log --no-spec --flat

在创建出的文件中打印以下前后

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LogMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    console.log('before2', req.url);

    next();

    console.log('after2');
  }
}

然后在 AppModule 中启用该中间件

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LogMiddleware).forRoutes('person*');
  }
}

image

其中 forRoutes 中就是指定该中间件在匹配到哪些路由时可以生效
image

路由中间件可以明确或者泛指定哪些路由可以使用该中间件,其余是无法触发该中间件的
全局中间件则是所有的路由都会触发该中间件,没有任何约束

Guard

Guard 是路由守卫的意思,可以用于在调用某个 Controller 之前判断权限,返回 true 或者 false 来决定是否放行。

创建一个 Guard

nest g guard login --no-spec --flat

Guard 需要实现 CanActivate 接口,实现 canActivate 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。

简单使用

对生成的 guard 文件内容返回 false,

@Injectable()
export class LoginGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // 其中 context 是该此请求调用的执行上下文
  // 比如 context.getClass(), context.getHandler() 得到的打印就是 [class PersonController] [Function: findAll]
    console.log('guard log');

    return false;
  }
}

其中 context 参数是执行上下文,可以用来获取此次调用的上下文信息,比如context.getClass() context.getHandler(),访问 /person,得到的打印就是如下图所示内容,其中 context.getClass() 获取当前路由的类,context.getHandler() 可以获取到路由将要执行的方法

image

在某个请求(此处为 PerosnController 的 /person 路由)的 Controller 上添加 @UseGuards(LoginGuard)

image

然后访问 http://localhost:3000/person ,请求报错,并返回无权限的提示信息

image

就像这样,Controller 本身不需要做啥修改,只需要加一个装饰器,就加上了权限判,这就是 AOP 架构的好处。

而且,就像 Middleware 支持全局级别和路由级别一样,Guard 也可以全局启用

全局守卫

guard 的全局守卫有两种添加方式,可以在 main.ts 的 app 中添加,也可以在 AppModule 中添加,但是两者之间在使用上是有一些不同。

main.ts 中添加全局守卫

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 添加全局守卫
  app.useGlobalGuards(new LoginGuard());

  await app.listen(3000);
}

此时由于 LoginGuard 中直接写死返回 false,所以访问任何一个路由都是 403 无权

AppModule 中添加全局守卫

在 AppModule 中添加时,需要注意,全局守卫是在 providers 中进行关联,且其 proovide 提供的 token,必须是由 @nestjs/core 导出的 APP_GUARD

import { APP_GUARD } from '@nestjs/core';
import { LoginGuard } from './login.guard';

@Module({
  imports: [PersonModule],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,  // 注入的 token 必须是由 @nestjs/core 导出的
      useClass: LoginGuard,
    },
  ],
})

两种注册方式的区别

main.ts 中,由 app 注册的,是手动 new 的实例,是不在 IOC 容器中的,无法执行注入操作

@Injectable()
export class LoginGuard implements CanActivate {
  // 注入一个服务,调用其方法
  @Inject(PersonService)
  private readonly personService: PersonService;

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log('guard log', this.personService.findAll());

    return false;
  }
}

在使用 app 注册全局守卫时,访问任何一个路由都报错,无法找到注入的服务模块。

image

而如果使用 AppModule 注册全局守卫,由于 Guard 是在 IOC 容器中,所以可以正常执行注入操作

image

当模块需要注入其它 provider 时,则需要在 AppModule 中进行注册声明

Interceptor

Interceptor(拦截器) 可以处理请求处理过程中的请求和响应,例如身份验证、日志记录、数据转换等,其作用和 Middleware 较为相似,但却有明显的不同之处。可参考文档 Nest 中文网-拦截器

新建一个 interceptor 模板:

nest g interceptor test --no-spec --flat
// test.interceptor.ts
@Injectable()
export class TestInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
  //  这里的 context 和 Guard 的 context 差不多
    console.log(context.getClass(), context.getHandler());

    return next.handle();
  }
}

main.ts 中进行全局注册

import { TestInterceptor } from './test.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new TestInterceptor());
  await app.listen(3000);
}

拦截器的简单应用

interceptor 与 middleware 其中不同的一点就是,interceptor 可以获取到数据信息,并对其进行操作,rjsx 中提供了多种功能,例如 map tap 等。
同时 interceptor 内部可以获取到 context,和 guard 一样,而 middleware 是不行的。

  • 对返回数据进行格式化包装
export interface Response<T = any> {
  code: number;
  data: T;
}

@Injectable()
export class TestInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response> {
	// 返回的结果都会被格式化为 {code:xxx, data:...} 的格式
    return next.handle().pipe(map((data) => ({ code: 200, data })));
  }
}

image

拦截器的全局注册和局部注册

interceptor 的注册方式和 guard 类似

  • 全局注册(方式一:app 注册)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(new TestInterceptor());
  await app.listen(3000);
}
  • 全局注册(方式二:AppModule 注册)
@Module({
  imports: [PersonModule],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,  // 该常量由 @nestjs/core 导出
      useClass: TestInterceptor,
    },
  ],
})
  • 局部注册(在 Controller 添加装饰器)
@Controller('person')
//  @UseInterceptors(LoginGuard)  // 当写在这里时,表明此拦截器将作用于该 Controller 下的所有 handler 内容
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Get()
  @UseInterceptors(LoginGuard)  // 使用 @UseInterceptors 添加局部拦截器
  findAll() {
    return this.personService.findAll();
  }
}

Pipe

管道的主要作用就是对参数进行校验和转换。Nest 中文网-管道

自定义管道

  • 创建 pipe 管道模板
nest g pipe validate --no-spec --flat
  • 对管道模板进行自定义的参数校验
@Injectable()
export class ValidatePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (Number.isNaN(parseInt(value))) {
      throw new BadRequestException(`参数${metadata.data}错误`);
    }

    return typeof value === 'number' ? value * 10 : parseInt(value) * 10;
  }
}

  • 创建一个请求
import { ValidatePipe } from 'src/validate.pipe';

@Controller('person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Get('/add')  // @Query 接收第二个参数,即刚才的自定义校验管道
  addNum(@Query('num', ValidatePipe) num: number) {
    return num + 1;
  }
}

image

image

局部管道和全局管道

  • 全局管道也是两种注册方式(app 和 AppModule)

image

  • 局部管道

image

Nest 内置 Pipe

Nest 附带了九个开箱即用的管道

  • ValidationPipe
    • ValidationPipe 较为特殊,详细使用请查看中文网文档:参考链接
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

简单使用

  • ParseIntPipe 只允许整数 int 类型的数字通过校验
import { ParseIntPipe } from '@nestjs/common';

@Get('/add')
addNum(@Query('num', ParseIntPipe) num: number) {
  return num + 1;
}

image

更多便捷参数验证请查看 Nest 中文网-管道#类验证器

自定义内置管道的内容

当需要返回自定义的错误状态码时,可以实例化内置管道

@Get('/add')
addNum( @Query('num', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }) ) num: number ) {
  return num + 1;
}

image

ExceptionFilter

Nest 内置的异常层,所有未经处理的异常错误,该异常层就会进行捕获,并返回对应的响应信息,相当于内置的一个全局异常拦截器。Nest 中文网-异常过滤器
比如手动 throw new BadRequestException(...) 就会被默认内置的异常层捕获

nest g filter ex --no-spec --flat

对抛出的 BadRequestException 进行单独的异常过滤

import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express';  // Request 一定是从 express 中导出,而不是 @nestjs/common

@Catch(BadRequestException)  // 捕获 BadRequestException 异常
export class HttpFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

局部使用该异常过滤器

  @Get('/add')
  @UseFilters(HttpFilter)
  addNum(@Query('num', ValidatePipe) num: number) {
    return num + 1;
  }

image

http 相关的内置异常

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

也可以自行扩展,自定义异常

export class MyException extends HttpException {
  constructor() {
    super('Custom Error', 999);
  }
}

全局注册和局部注册

  • 全局异常过滤器的两种注册方式和管道、守卫等一致(app 和 AppModule)
// main.ts 中注册
app.useGlobalFilters(new HttpFilter());

//  AppModule 中注册
@Module({
  providers: [
    AppService,
    {
      provide: APP_FILTER,  // // 该常量由 @nestjs/core 导出
      useClass: HttpFilter,
    },
  ],
})
  • 局部注册,可作用于单个 handler 指定异常,也可作用于整个 Controller 的 handler 指定异常
@Controller('person')
// @UseFilters(HttpFilter) 作用于该 controller 下的所有捕获到的指定异常
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Get('/add')
  @UseFilters(HttpFilter)  // 只作用于该 handler 的指定异常
  addNum(@Query('num', ValidatePipe) num: number) {
    return num + 1;
  }
}

AOP 的执行顺序

该图来自于 stackoverflow。原文地址

image

Middleware 是 Express 的概念,Nest 将其放在最外层执行。到了某个路由之后,会先调用 GuardGuard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。在此期间,不论是 Pipe 校验抛出的异常或是其它所有的 HttpException 的异常都会被 ExceptionFilter 处理,最后返回不同的响应。

标签:架构,app,中间件,num,Nestjs,context,AOP,AppModule,全局
From: https://www.cnblogs.com/jsonq/p/18050644

相关文章

  • 【个人前端笔记】Node.js技术架构
    一:node.js不是什么1.node.js不是web框架或后端框架所以你不能把Node.js与Flask或Spring对比2.node.js不是编程语言node.js并不是后端的JS,它只是以.js做后缀的所以你不能把Node.js与Python或PHP对比二:node.js是什么1.node.js是一个平台它将多种技术组合起来让Javascript也......
  • 系统架构设计师学习(一)未来信息综合技术
    一、引言本来是想着按教材顺序来进行编写的,但是出于个人喜好,我阅读的第一章即本文所描述的未来信息综合技术走向,所以就按我阅读的顺序来进行整理了。2024年其实我个人感觉到非常大的危机了,不管是大环境还是AI对我们行业的冲击,我觉得有必要要重新审视当前的自己并做出一......
  • Spring 的 IOC 和 AOP 是什么,有哪些优点?
    Spring框架中的IOC是**控制反转**,AOP是**面向切面编程**。IOC是Spring框架的核心特性之一,它代表的是控制反转,意味着将对象的创建和管理交给Spring容器,而不是传统的在对象内部进行控制。这样可以实现对象之间的解耦,提高代码的可维护性和灵活性。IOC的底层原理包括XML解析、工厂模......
  • 阅读《架构漫谈》后对于架构的理解
    在信息技术日新月异的今天,软件架构作为连接业务需求和代码实现的重要桥梁,越来越受到业界的关注。我深入阅读了资深架构师王概凯所著的《架构漫谈》系列专栏,深感其对于软件架构的独到见解和深入剖析。本文将从对架构概念的理解、架构的重要性和实践方法等方面展开论述,旨在探讨如何......
  • Nestjs系列 Nestjs基础(二)
    providers使用该内容可以结合Nestjs中文网-自定义提供者查看创建一个nest项目,创建一个Personcrud模块。providers写法providers完整和简写@Injectable()装饰器将PersonService类标记为提供者。然后在Module中声明,即和PersonService做关联,个人感觉provider......
  • 架构漫谈观后感
     《架构漫谈:王概凯的技术思考》是一本探讨软件架构设计和技术创新的书籍,作者王概凯凭借其丰富的实践经验和深刻的技术洞察力,为读者展开了一场关于软件架构的深度对话。读完这本书后,我被作者对技术的热情、对架构设计的深刻理解以及对未来技术趋势的敏锐洞察所深深吸引。以下是我......
  • 架构漫谈
    《架构漫谈》是一本深刻探讨计算机系统架构的书籍,对于理解和设计复杂系统的架构提供了有价值的见解。以下是对这本书的1500字读后感:《架构漫谈》一书是一部引人深思的计算机科学巨著,以其深度的洞察力和独到的观点,为读者呈现了计算机系统架构的精髓。通过对书中内容的深入学习,我深......
  • 《大型网站技术架构:核心原理与案例分析》读后感
    《大型网站技术架构:核心原理与案例分析》这本书,对我而言,不仅仅是一本介绍技术架构的专著,它更是一次深入探索互联网技术奥秘的精神之旅。作者李智慧以其丰富的行业经验和深厚的技术底蕴,为我们揭开了大型网站背后复杂架构的神秘面纱。在阅读第一章后,我被作者系统化、层次分明的叙述......
  • 《架构漫谈》读后感
    《架构漫谈》读后感——王概慨的智慧启迪与深度思考在阅读完王概慨先生的著作《架构漫谈》后,我深受启发,深感架构设计之于现代信息技术领域的重要性以及其背后所蕴含的深厚理论基础与实践经验。全书以生动的笔触和丰富的实例,对软件架构的各个方面进行了深入浅出的探讨,不仅拓展了我......
  • 《架构漫谈》读后感
    在王老师的推荐下阅读了王概凯的架构漫谈。1.什么是架构:架构是指系统的基本组织结构或设计框架,包括系统的各种组件、它们之间的相互关系以及对外部环境的接口。在软件开发中,架构定义了软件系统的整体结构,包括软件的分层、模块化、组件化等方面,以及系统中各个部分的职责和交互方式......