购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4 Nest的AOP架构理念

Nest采用了多种优秀的设计模式和软件设计思想,其中之一就是面向切面编程(AOP)。AOP的引入解决了传统面向对象编程中的一些问题,比如代码重复和业务逻辑的混杂。作为Nest最核心的设计理念之一,本节将深入学习AOP的工作机制及其应用。

2.4.1 MVC架构概述

Nest属于MVC(Model-View-Controller,模型-视图-控制器)架构体系,实际上,大多数后端框架都是基于这一架构设计的。在MVC架构中:

● Model层负责业务逻辑处理,包括数据的获取、存储、验证以及数据库操作。

● Controller层通常用于处理用户的输入,调度Service服务,以及进行API的路由管理。

● View层在传统的服务器端渲染中,可能使用如ejs、hbs等模板引擎。在前后端分离的体系中,通常指的是客户端框架(如Vue或React)负责的部分。

当一个HTTP请求到达服务器时,它首先会被Controller层接收。Controller层会根据请求调用Model层中的相应模块来处理业务逻辑,并将处理结果返回给View层以进行展示。整个基础流程的示意图如图2-31所示。

图2-31 HTTP请求基本流程

在MVC架构的基础上,Nest还引入了AOP的思想,从而具备了面向切面编程的能力。我们经常听到后端开发者提到AOP切面,那么究竟什么是面向切面编程呢?接下来将给出答案。

2.4.2 AOP解决的问题

以一个HTTP请求为例,客户端发送请求时首先会经过Controller(控制器)、Service(服务)、DB(数据访问或操作)等模块。如果想要在这些模块中加入一些操作,例如数据验证、权限校验或日志统计,应该怎么办呢?

首先,我们可能会想到在Controller中加入参数校验逻辑或者权限校验,如果不通过验证,就直接返回错误。这样看起来似乎没什么问题。但是,如果有多个功能模块都需要进行校验,并且权限校验的逻辑相同,那么是不是意味着需要在多个Controller中重复加入这段逻辑?显然,这样做会导致公共逻辑与业务逻辑耦合。有没有办法可以做到统一管理呢?

答案是有的,办法如图2-32所示。

图2-32 切面工作原理图

由图2-32可见,在Controller的前后都可以“切一刀”,用来统一处理公共逻辑,这样就不会侵入Controller、Service等业务代码。

事实上,在Nest中,请求流程可以换一种角度来看,如图2-33所示。

图2-33 Nest请求流程

中间的灰色区域属于AOP切面部分,包含Middleware(中间件)、Guard(守卫)、Interceptor(拦截器)、Pipe(管道)和Filter(过滤器)。它们都是AOP思想的具体实现。

2.4.3 AOP在Nest中的应用

前面我们了解了AOP思想的优势,接下来看看AOP在Nest中的应用。这里将以store-web项目作为演示,代码已经放到GitHub对应章节的目录下。

1.中间件

Nest的中间件默认是基于Express的,它在请求流程中的位置如图2-34所示。

图2-34 中间件的位置

中间件可以在路由处理程序之前或之后插入执行任务,它们分为全局中间件和局部中间件两种类型。

全局中间件通过use方法调用,与Express中的使用方式类似。所有进入应用的请求都会经过全局中间件,通常用于执行日志统计、监控、安全性处理等任务。例如,在main.ts文件中使用全局中间件的方式如下,其中LoggerMiddleware是一个用于日志统计的中间件。示例代码如下:

     async function bootstrap() {
       const app = await NestFactory.create(AppModule);
       // 中间件
       app.use(new LoggerMiddleware().use)
       // 启动服务
       await app.listen(8088);
     } 
     bootstrap();

局部中间件通常应用于特定的控制器或单个路由上,以实现更细粒度的逻辑控制。例如,可以将LoggerMiddleware绑定到/person/create-person路由,并指定其适用的HTTP请求方法。以下是如何在控制器中应用局部中间件的示例代码:

对于中间件的更深入的知识,我们将在后续章节中进行详细介绍。

2.守卫

守卫的职责很明确,通常用于权限、角色等授权操作。守卫所在的位置与中间件类似,可以对请求进行拦截和过滤,如图2-35所示。

图2-35 守卫的位置

守卫在调用路由程序之前返回true或者false来判断是否通行,分为全局守卫和局部守卫。

守卫必须实现CanActivate接口中的canActivate()方法,代码如下:

全局守卫在main.ts中通过useGlobalGuards来调用,每个路由程序都会经过它进行权限验证才能够通行。

     async function bootstrap() {
       const app = await NestFactory.create(AppModule);
       // 守卫
       app.useGlobalGuards(new PersonGuard())
       // 启动服务
       await app.listen(8088);
     } 

同样,与中间件类似,作为局部守卫,可以缩小控制范围,从而实现更加精细的权限控制。控制器中的示例代码如下:

     @Controller('person')
     // 声明守卫
     @UseGuards(new PersonGuard())
     // 控制器
     export class PersonController {}
3.拦截器

拦截器不同于中间件和守卫,它在路由请求之前和之后都可以进行逻辑处理,能够充分操作request和response对象,如图2-36所示。拦截器通常用于记录请求日志、转换或格式化响应数据等。

图2-36 拦截器的位置

为了更好地说明拦截器的作用,下面的代码定义了一个用于统计接口超时的拦截器:

在上述代码中,拦截器必须实现NestInterceptor接口中的intercept方法。该方法包含与守卫相同的ExecutionContext上下文对象作为第一个参数。第二个参数是CallHandler,它代表每个路由处理程序的方法。只有当拦截器调用了handle方法后,控制权才会交给路由处理程序。在Nest中,拦截器通常与RxJS异步处理库一起使用,以执行一些异步逻辑,例如上例中的超时(timeout)统计。

类似于守卫,拦截器可以设置为控制器作用域、方法作用域或全局作用域。

(1)控制器作用域允许拦截器只作用于某个控制器。当程序执行到控制器时触发拦截器逻辑,通过@UseInterceptors装饰器将TimeoutInterceptor绑定到控制器类,代码如下:

     @Controller('person')
     // 为控制器绑定超时拦截器
     @UseInterceptors(new TimeoutInterceptor())

     export class PersonController {}

(2)方法作用域把拦截器的作用范围限制在某个方法上,比全局或控制器级别的范围更加精确。当程序执行到该方法时,触发拦截器的逻辑。绑定方式如下:

     @Get()
     // 为单独的方法绑定超时拦截器
     @UseInterceptors(new TimeoutInterceptor())
     findAll() {
       return this.personService.findAll();
     }

(3)全局作用域允许拦截器应用到整个应用中。在main.ts文件中,可以通过app.useGlobalInterceptors()方法进行绑定,代码如下:

     async function bootstrap() {
       const app = await NestFactory.create(AppModule);
       // 全局超时拦截器
       app.useGlobalInterceptors(new TimeoutInterceptor());
       // 启动服务
       await app.listen(8088);
     } 
     bootstrap();
4.管道

管道用于处理通用逻辑,其中两个典型的用例是处理请求参数的验证(validation)和转换(transformation)。在执行路由方法之前,会首先执行管道逻辑,并将经过管道转换后的参数传递给路由方法。它的位置如图2-37所示。

图2-37 管道的位置

尽管Nest框架中已经内置了一些管道(Pipes),例如ParseIntPipe,但有时我们可能需要实现特定的转换功能,比如将数字转换为八进制。在这种情况下,我们需要创建自定义管道。以下是一个自定义管道的示例代码:

自定义管道需要实现PipeTransform接口的transform()方法。其中,value表示需要处理的方法参数,而metadata则是描述该参数的元数据,用于标识其属于哪种类型,如'body'、'query'、'param'和'custom'。

除了自定义管道和内置管道外,通常我们还会结合第三方验证库,例如class-validator。在后面的章节中将会详细介绍这部分内容。

如果管道验证器验证失败,则需要抛出异常并回应给客户端。这时就需要使用异常过滤器来处理。

5.过滤器

Nest中最为常见的是HTTP异常过滤器,通常用于在后端服务发生异常时向客户端报告异常的类型。目前内置的HTTP异常包含:

● BadRequestException

● UnauthorizedException

● NotFoundException

● ForbiddenException

● NotAcceptableException

● RequestTimeoutException

● ConflictException

● GoneException

● HttpVersionNotSupportedException

● PayloadTooLargeException

● UnsupportedMediaTypeException

● ...

它们都继承自HttpException类。当然,我们也可以自定义异常过滤器,并向前端返回统一的数据格式:

自定义异常过滤器需要实现ExceptionFilter接口的catch()方法来拦截异常。ArgumentsHost能够获取不同平台的传输协议上下文,用于访问request和response对象。

另外,@Catch()装饰器用于声明要拦截的异常类型,这里使用的是HttpException。异常过滤器可应用于控制器作用域、方法作用域和全局作用域,我们都会尝试一遍。

(1)将过滤器绑定到控制器,代码如下:

     @Controller('person')
     // 绑定到控制器
     @UseFilters(new HttpExceptionFilter())
     export class PersonController {}

(2)将过滤器绑定到某个路由方法中,代码如下:

     @Get()
     // 绑定到路由方法
     @UseFilters(new HttpExceptionFilter())
     findAll() {
       return this.personService.findAll();
     } 

(3)将过滤器绑定到全局中,代码如下:

     async function bootstrap() {
       const app = await NestFactory.create(AppModule);
       // 全局异常过滤器
       app.useGlobalFilters(new HttpExceptionFilter());
       // 启动服务
       await app.listen(8088);
     } 
     bootstrap();

以上就是Nest实现AOP架构思想的几种方式。不同的切面解决不同场景下的通用逻辑抽离问题,从而实现了更加灵活和可维护的应用程序。 x9ZWsBiWT1T+eyOIEjAl5ss5wV/hP+3tiLl2j3qclzIVQWjWkSebSnp8Xt7BXWkS

点击中间区域
呼出菜单
上一章
目录
下一章
×