视频教程
nestjs 全栈进阶--nest生命周期
1. 概念
面向切面编程(Aspect-Oriented Programming,简称AOP)是一种编程范式,其核心理念在于将交叉-cutting concerns(横切关注点)从主业务逻辑中抽离出来,以便于管理和重用。这些横切关注点通常包括日志记录、事务管理、安全检查、性能监控等,它们贯穿于整个系统,可能会影响到多个类或多个方法。
在传统的面向对象编程(Object-Oriented Programming,简称OOP)中,尽管我们可以很好地组织和封装对象的内部状态和行为,但对于那些与业务逻辑并非紧密关联,却需要在多个地方重复调用或应用的功能,OOP无法直接有效地解决其分散和冗余的问题。AOP就是为了解决这一问题而提出的。
2. aop的关键概念
- 切面(Aspect) : 切面是AOP的基本单元,它包含了横切关注点的全部实现。一个切面可以包含多个通知(Advice),并定义了何时何地执行这些通知。
- 通知(Advice) : 通知是切面的具体实现,描述了在程序执行过程中何时以及如何插入额外的代码。常见的通知类型有前置通知(Before)、后置通知(After Returning/Throwing)、环绕通知(Around)等。
- 连接点(Join Point) : 连接点是程序执行过程中的某个特定位置,比如方法调用、异常抛出等时刻。通知可以在这些连接点上织入(Weaving)到程序中。
- 切入点(Pointcut) : 切入点是匹配连接点的一组规则,它定义了哪些连接点会被通知所织入。例如,我们可以定义一个切入点来表示所有以特定名字开头的方法。
- 织入(Weaving) : 织入是指将切面应用到目标对象来创建新的代理对象的过程。织入可以在编译时、加载时或运行时进行。
3. 应用场景
- 日志记录:在每个方法调用前后自动记录日志,无需在每个业务方法中手动编写日志代码。
- 事务管理:跨多个业务方法进行事务控制,保证数据一致性,无需在每个涉及数据库操作的方法内显式开启、提交或回滚事务。
- 性能监控:在方法执行前后统计耗时,方便分析系统瓶颈。
- 权限验证:在访问敏感资源前进行权限检查,而不是在每个资源访问方法里嵌入验证代码。
4. 图示
一个请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑, 如果想在这个调用链路里加入一些通用逻辑该怎么加?比如日志记录、权限控制、异常处理等。
最直接的就是修改Controller 层代码,但是不优雅,所以我们应该考虑在Controller 之前或之后加入一个执行通用逻辑的阶段,比如:
这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP (面向切面编程)。AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑可以复用,还可以动态的增删。
5. nest中实现AOP的主要方式
- 中间件 (middleware)
- 中间件是 Express 里的概念,Nest 的底层是 Express,所以自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件
- 拦截器(Interceptor)
- 拦截器是AOP中的一个重要组件,它能够捕获并处理到特定服务或控制器方法的HTTP请求和响应。拦截器可以在请求到达实际处理方法前执行预处理逻辑(前置逻辑),并在响应发送给客户端之前执行后置处理逻辑。这种机制可用于实现诸如日志记录、权限验证、性能监控、统一错误处理等横切关注点。
- 守卫(Gurad)
- 守卫(Guard)也是一种特殊的拦截器,主要用于控制路由访问权限。守卫可以根据特定条件决定是否允许请求继续执行到后续的处理器。这种特性在处理用户认证、授权等与业务逻辑分离的安全相关问题时非常有用。
- 异常过滤器(ExceptionFilter)
- 异常过滤器专门用来处理全局或局部范围内的未捕获异常,它可以捕获并转换异常,甚至完全替代原始异常信息,以便向客户端提供更友好的错误响应。这确保了系统中有关异常处理的横切关注点得以集中管理。
- 管道(pipe)
- 虽然管道主要负责参数转换和验证,但也可以看作是对AOP的一种变通实现,因为它能在执行链路中插入一个环节来处理所有传递给控制器方法的参数,从而实现对输入数据的统一处理和校验,这也是一种横切关注点的体现。
6. 代码体验
arduino
nest new aop -p pnpm
sql
pnpm start:dev
6.1. 中间件
- 全局中间件
现在我们可以看到 当我们请求时,我们在当前请求前后动态的增加了一些可复用的逻辑。
- 路由中间件
css
nest g middleware logger --no-spec --flat
这是一个生成中间件的命令,上图是他生成的代码
然后我们在多加几个请求,并修改下他生成的中间件代码
然后在 AppModule 里启用,在 configure 方法里配置 LogMiddleware 在哪些路由生效。
可以看到,只有 aaa 的路由,中间件生效了
后面我们还会单独讲中间件,现在我们先体验一下
6.2. 守卫(Guard)
守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate 接口。
守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。
tips: 守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
css
nest g guard login --no-spec --flat
生成的代码, 守卫要求实现CanActivate 给定参数context执行上下文 要求返回布尔值
我们加个打印语句,然后返回 false,之后在 AppController 里启用
在浏览器访问,因为我们返回的false,代表没有权限,所以返回了403
可以看到他是在中间件执行后执行的
- 全局守卫
使用方式1:
使用方式2:
注释掉我们刚刚在main.ts中添加的代码,然后
这两种全局守卫的区别:
第一种(main中那种):是手动 new 的 Guard 实例,不在 IoC 容器里
第二种:用 provider 的方式声明的 Guard 是在 IoC 容器里的,可以注入别的 provider,比如
6.3. 拦截器(Interceptor)
拦截器是用 @Injectable() 装饰器注释并实现 NestInterceptor 接口的类。
拦截器具有一组有用的功能,这些功能的灵感来自 面向方面编程 (AOP) 技术。它们可以:
- 在方法执行之前/之后绑定额外的逻辑
- 转换函数返回的结果
- 转换函数抛出的异常
- 扩展基本功能行为
- 根据特定条件完全覆盖函数(例如,出于缓存目的)
css
nest g interceptor duration --no-spec --flat
生成的代码
我们改造一下,并屏蔽全局守卫,且修改守卫代码
我们再去启用这个拦截器
Interceptor 除了支持每个路由单独启用,也可以在 controller 级别启动,作用于下面的全部 handler:
也同样支持全局启用
或
Interceptor 和 Middleware的区别:
interceptor 可以拿到调用的 controller 和 handler:
以下是Nest.js中的中间件、守卫、拦截器对比的总结:
类型 | 功能与用途 | 工作流程与接口 | 示例场景 |
---|---|---|---|
中间件 | - 对HTTP请求和响应进行预处理和后处理- 可修改请求或响应对象- 可短路请求处理链 | - 应用于全局、模块或路由层级 | - 全局错误处理- 日志记录- 身份验证(基础层面)- CORS配置 |
守卫(Guards) | - 决定是否允许执行后续的控制器方法- 主要负责权限验证和资源访问保护 | - 实现CanActivate等接口 | - 用户登录验证- 权限检查- IP过滤 |
拦截器(Interceptors) | - 在调用服务或控制器方法前后插入额外行为- 可修改请求参数、处理结果或响应本身 | - 实现Interceptor接口 | - 统一错误处理与响应格式化- 添加请求/响应头- 缓存策略- 请求日志 |
6.4. 管道(pipe)
管道是用 @Injectable() 装饰器注释的类,它实现了 PipeTransform 接口。
管道有两个典型的用例:
转型:将输入数据转换为所需的形式(例如,从字符串到整数)
验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常
在这两种情况下,管道都在由 控制器路由处理器 处理的 arguments 上运行。Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生,之后会使用任何(可能)转换的参数调用路由处理程序。
css
nest g pipe validate --no-spec --flat
Pipe 要实现 PipeTransform 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。
修改下他生成的代码
typescript
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@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 + 1 : parseInt(value) + 1;
}
}
然后应用这个 pipe
同样,Pipe 除了对某个参数生效,或者整个 Controller 都生效(注意,只对有参数的路由生效)
或者全局生效(注意,只对有参数的路由生效)
Nest 内置Pipe
- ValidationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
- ParseEnumPipe
- ParseFloatPipe
- ParseFilePipe
6.5. 异常过滤器(ExceptionFilter)
Nest 带有一个内置的异常层,负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。
开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理 HttpException 类型(及其子类)的异常。当异常无法识别时(既不是 HttpException 也不是继承自 HttpException 的类),内置异常过滤器会生成以下默认 JSON 响应:
json
{
"statusCode": 500,
"message": "Internal server error"
}
还记得我们刚刚返回400错误的响应吗?他就是 Exception Filter 做的
css
nest g filter test --no-spec --flat
我们来修改一下,我们去实现 ExceptionFilter 接口,实现 catch 方法,这样就可以拦截异常了,拦截什么异常用 @Catch 装饰器来声明,然后在 catch 方法返回对应的响应,给用户更友好的提示。
typescript
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
@Catch(BadRequestException)
export class TestFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const response: Response = host.switchToHttp().getResponse();
response.status(400).json({
statusCode: 400,
message: 'test: ' + exception.message
})
}
}
应用一下
Nest 通过这样的方式实现了异常到响应的对应关系,代码里只要抛出不同的异常,就会返回对应的响应.
同样,ExceptionFilter 除了对单个路由生效也可以对整个整个 Controller 都生效
或者,全局生效
或
Nest 内置了很多 http 相关的异常,都是 HttpException 的子类:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
6.6. 执行顺序
Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。