夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
      • MVC
      • AOP
        • 中间件 Middleware
        • 全局中间件
        • 路由中间件
        • 守卫 Guard
        • 路由守卫
        • 全局守卫
        • 拦截器 Interceptor
        • 单独启用
        • 全局启用
        • 参数检验和转换 Pipe
        • 内置 Pipe
        • 单独或全局使用
        • 异常处理 ExceptionFilter
        • 结合 validatePipe 处理异常
        • 内置 HttpException
        • 扩展 HttpException
        • 单独或全局使用
        • 几种 AOP 机制的顺序
        • 图例
        • 详解
        • 最佳实践
      • 总结
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
  • 其他

  • 服务端
  • Nest
神说要有光
2025-03-10
目录

AOP 架构有什么好处?

# MVC

后端框架基本都是 MVC 的架构。

MVC 是 Model View Controller 的简写。MVC 架构下,请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。

# AOP

在这个流程中,Nest 还提供了 AOP (Aspect Oriented Programming)的能力,也就是面向切面编程的能力。

AOP 是什么意思呢?什么是面向切面编程呢?

一个请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑:

如果想在这个调用链路里加入一些通用逻辑该怎么加呢?比如日志记录、权限控制、异常处理等。

容易想到的是直接改造 Controller 层代码,加入这段逻辑。

这样可以,但是不优雅,因为这些通用的逻辑侵入到了业务逻辑里面。能不能透明的给这些业务逻辑加上日志、权限等处理呢?

那是不是可以在调用 Controller 之前和之后加入一个执行通用逻辑的阶段呢?

比如这样:

是不是就和切了一刀一样?

这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP (面向切面编程)。

AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑可以复用,还可以动态的增删。

其实 Express 的中间件的洋葱模型也是一种 AOP 的实现,因为你可以透明的在外面包一层,加入一些逻辑,内层感知不到。

而 Nest 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Interceptor、ExceptionFilter。

新建个 nest 项目,我们挨个试一下:

nest new aop-test
1

# 中间件 Middleware

中间件是 Express 里的概念,Nest 的底层是 Express,所以自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件。

# 全局中间件

全局中间件就是这样:

在 main.ts 里通过 app.use 使用:

app.use(function(req: Request, res: Response, next: NextFunction) {
    console.log('before', req.url);
    next();
    console.log('after');
})
1
2
3
4
5

在 AppController 里也加个打印:

把服务跑起来:

npm run start:dev
1

浏览器访问下:

可以看到,在调用 handler 前后,执行了中间件的逻辑。

我们再添加几个路由:

@Get('aaa')
aaa(): string {
    console.log('aaa...');
    return 'aaa';
}

@Get('bbb')
bbb(): string {
    console.log('bbb...');
    return 'bbb';
}
1
2
3
4
5
6
7
8
9
10
11

然后浏览器访问下:

可以看到,中间件逻辑都执行了:

也就是说,可以在多个 handler 之间复用中间件的逻辑:

这种可以给在 handler 前后动态增加一些可复用的逻辑,就是 AOP 的切面编程的思想。

# 路由中间件

除了全局中间件,Nest 还支持路由中间件。

用 nest cli 创建一个路由中间件:

nest g middleware log --no-spec --flat
1

--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');
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后在 AppModule 里启用:

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LogMiddleware } from './log.middleware';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule{

  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LogMiddleware).forRoutes('aaa*');
  }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在 configure 方法里配置 LogMiddleware 在哪些路由生效。

然后测试下:

可以看到,只有 aaa 的路由,中间件生效了。

这就是全局中间件和路由中间件的区别。

# 守卫 Guard

# 路由守卫

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

我们创建个 Guard:

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

生成的 Guard 代码是这样的:

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

我们加个打印语句,然后返回 false:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class LoginGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log('login check')
    return false;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13

之后在 AppController 里启用:

然后再访问下:

aaa 没有权限,返回了 403。

Controller 本身不需要做啥修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。

# 全局守卫

# useGlobalGuards

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

这样每个路由都会应用这个 Guard:

# 注入

还有一种全局启用的方式,是在 AppModule 里这样声明:

{
  provide: APP_GUARD,
  useClass: LoginGuard
}
1
2
3
4

把 main.ts 里的 useGlobalGuards 注释掉:

再试下:

可以看到,Guard 依然是生效的。

那为什么都是声明全局 Guard,需要有两种方式呢?

因为之前这种方式是手动 new 的 Guard 实例,不在 IoC 容器里:

而用 provider 的方式声明的 Guard 是在 IoC 容器里的,可以注入别的 provider:

我们注入下 AppService 试试:

@Inject(AppService)
private appService: AppService;
1
2

浏览器访问下:

可以看到,注入的 AppService 生效了。

所以,当需要注入别的 provider 的时候,就要用第二种全局 Guard 的声明方式。

当使用此方法为守卫程序执行依赖项注入时,请注意,无论使用此构造的模块是什么,守卫程序实际上是全局的。应该在哪里进行?选择定义守卫的模块(上例中的 RolesGuard)。此外,useClass不是处理自定义 providers 注册的唯一方法。在这里 (opens new window)了解更多。

# 拦截器 Interceptor

Interceptor 是拦截器的意思,可以在目标 Controller 方法前后加入一些逻辑:

创建个 interceptor:

nest g interceptor time --no-spec --flat
1

生成的 interceptor 是这样的:

Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会调用目标 Controller,可以在之前和之后加入一些处理逻辑。

Controller 之前之后的处理逻辑可能是异步的。Nest 里通过 rxjs 来组织它们,所以可以使用 rxjs 的各种 operator。

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

@Injectable()
export class TimeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {

    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        console.log('time: ', Date.now() - startTime)
      })
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

把之前那个 LoginGuard 注掉:

然后启用这个 interceptor:

跑一下:

可以看到,interceptor 生效了。

有的同学可能会觉得 Interceptor 和 Middleware 差不多,其实是有区别的,主要在于参数的不同。

interceptor 可以拿到调用的 controller 和 handler:

后面我们会在 controller 和 handler 上加一些 metadata,这种就只有 interceptor或者 guard 里可以取出来,middleware 不行。

# 单独启用

Interceptor 支持每个路由单独启用,只作用于某个 handler:

也可以在 controller 级别启动,作用于下面的全部 handler:

# 全局启用

也同样支持全局启用,作用于全部 controller:

两种全局启用方式的区别和 guard 的一样,就不测试了。

除了路由的权限控制、目标 Controller 之前之后的处理这些都是通用逻辑外,对参数的处理也是一个通用的逻辑,所以 Nest 也抽出了对应的切面,也就是 Pipe:

# 参数检验和转换 Pipe

Pipe 是管道的意思,用来对参数做一些检验和转换:

用 nest cli 创建个 pipe:

nest g pipe validate --no-spec --flat
1

生成的代码是这样的:

Pipe 要实现 PipeTransform 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。

我们实现下:

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 * 10 : parseInt(value) * 10;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这里的 value 就是传入的参数,如果不能转成数字,就返回参数错误,否则乘 10 再传入 handler:

在 AppController 添加一个 handler,然后应用这个 pipe:

@Get('ccc')
ccc(@Query('num', ValidatePipe) num: number) {
    return num + 1;
}
1
2
3
4

访问下:

可以看到,参数错误的时候返回了 400 响应,参数正确的时候也乘 10 传入了 handler。

这就是 Pipe 的作用。

# 内置 Pipe

Nest 内置了一些 Pipe,从名字就能看出它们的意思:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe
  • ParseFilePipe

# 单独或全局使用

同样,Pipe 可以只对某个参数生效,或者整个 Controller 都生效:

或者全局生效:

不管是 Pipe、Guard、Interceptor 还是最终调用的 Controller,过程中都可以抛出一些异常,如何对某种异常做出某种响应呢?

这种异常到响应的映射也是一种通用逻辑,Nest 提供了 ExceptionFilter 来支持:

# 异常处理 ExceptionFilter

ExceptionFilter 可以对抛出的异常做处理,返回对应的响应:

其实我们刚刚在 pipe 里抛的这个错误,能够返回 400 的响应,就是 Exception Filter 做的:

创建一个 filter:

nest g filter test --no-spec --flat
1

生成的代码是这样的:

改一下:

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
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现 ExceptionFilter 接口,实现 catch 方法,就可以拦截异常了。

拦截什么异常用 @Catch 装饰器来声明,然后在 catch 方法返回对应的响应,给用户更友好的提示。

# 结合 validatePipe 处理异常

用一下:

再次访问,异常返回的响应就变了:

# 内置 HttpException

Nest 内置了很多 http 相关的异常,都是 HttpException 的子类:

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

# 扩展 HttpException

当然,也可以自己扩展:

Nest 通过这样的方式实现了异常到响应的对应关系,代码里只要抛出不同的异常,就会返回对应的响应,很方便。

# 单独或全局使用

同样,ExceptionFilter 也可以选择全局生效或者某个路由生效:

某个 handler:

某个 controller:

全局:

我们了解了 Nest 提供的 AOP 的机制,但它们的顺序关系是怎样的呢?

# 几种 AOP 机制的顺序

Middleware、Guard、Pipe、Interceptor、ExceptionFilter 都可以透明的添加某种处理逻辑到某个路由或者全部路由,这就是 AOP 的好处。

但是它们之间的顺序关系是什么呢?

调用关系这个得看源码了。

对应的源码是这样的:

很明显,进入这个路由的时候,会先调用 Guard,判断是否有权限等,如果没有权限,这里就抛异常了:

抛出的 ForbiddenException 会被 ExceptionFilter 处理,返回 403 状态码。

如果有权限,就会调用到拦截器,拦截器组织了一个链条,一个个的调用,最后会调用的 controller 的方法:

调用 controller 方法之前,会使用 pipe 对参数做处理:

会对每个参数做转换:

ExceptionFilter 的调用时机很容易想到,就是在响应之前对异常做一次处理。

而 Middleware 是 express 中的概念,Nest 只是继承了下,那个是在最外层被调用。

# 图例

这就是这几种 AOP 机制的调用顺序。把这些理清楚,就知道什么逻辑放在什么切面里了。

# 详解

根据NestJS官方文档的执行顺序:

  1. 中间件

    直接操作 Request 和 Response 对象。适用于所有路由或特定路径的预处理,如日志、请求头验证、CORS 设置、请求体解析(如 body-parser)。

  2. 守卫

    决定请求是否被允许继续处理(如身份认证、角色验证)。可访问 ExecutionContext,获取控制器、方法、请求参数等元数据。

  3. 拦截器(前置处理)

    在方法执行前插入逻辑,如数据转换、性能监控、异常处理。可获取 ExecutionContext。可拦截请求参数。通过 Observable 控制异步流程,支持 tap()、map() 等操作。

  4. 管道

    处理请求参数的格式转换(如字符串转数字)和验证(如校验输入是否符合规则)。通常绑定到单个请求参数或控制器方法。

  5. 控制器方法

    处理具体的 HTTP 请求,调用服务层(Service)完成业务操作。通过装饰器(如 @Get、@Post)定义路由路径和 HTTP 方法。

  6. 拦截器(后置处理)

    在方法执行后插入逻辑,如数据转换、性能监控、异常处理。可获取 ExecutionContext。可拦截控制器返回的响应数据。通过 Observable 控制异步流程,支持 tap()、map() 等操作。

  7. 异常过滤器

    捕获应用中抛出的异常,统一生成错误响应。将异常转换为特定的 HTTP 响应结构(如 { code, message })

# 最佳实践

  • 职责分离:确保每个组件专注于单一职责(如守卫只做权限校验)。
  • 合理分层:控制器方法应保持简洁,复杂逻辑委托给服务层。
  • 全局配置:优先使用全局管道、拦截器和异常过滤器,减少重复代码。
  • 错误处理:在服务层抛出语义化异常(如 NotFoundException),由过滤器统一转换。

案例代码在小册仓库 (opens new window)。

# 总结

Nest 基于 express 这种 http 平台做了一层封装,应用了 MVC、IOC、AOP 等架构思想。

MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。

IOC 是指 Nest 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。

AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。

Nest 的 Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。

我们通过源码来看了它们的调用顺序,Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。

Nest 就是通过这种 AOP 的架构方式,实现了松耦合、易于维护和扩展的架构。

AOP 架构的好处,你感受到了么?

编辑 (opens new window)
上次更新: 2025/5/14 16:47:16
全局模块和生命周期
一网打尽 Nest 全部装饰器

← 全局模块和生命周期 一网打尽 Nest 全部装饰器→

最近更新
01
IoC 解决了什么痛点问题?
03-10
02
如何调试 Nest 项目
03-10
03
Provider注入对象
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式