AOP 架构

后端框架都采用的是 MVC 架构。

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

image-20231003200111047

在 MVC 的基础上,Nest 还提供了 AOP(Aspect Oriented Programming)的能力,即面向切面编程。

面向切面编程的含义可以依据实际场景理解:

当请求过来时,会经过 Controller、Service、View。

如果有一些通用的逻辑,比如日志记录、权限控制、异常处理等,这些都应该放在哪一层?很明显,放在 Controller 层比较好,因为它用于控制。

但是 Controller 层我们一般会写很多跟业务相关的逻辑,如果再加入通用的逻辑,这一层的代码就会变得臃肿。

所以最好的处理方式是在 Controller 层之前或者之后加入一层处理通用逻辑的阶段。

img

这种横向的扩展点就叫做切面,加入一些切面逻辑的编程就叫做 AOP。

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

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

中间件 Middleware

中间件分全局中间件和路由中间件:

全局中间件是这么写的:

ts
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  await app.use(logger)
4  await app.listen(3000)
5}
6bootstrap()

路由中间件则是针对某个路由来说的:

ts
1export class AppModule implements NestModule {
2  configure(consumer: MiddlewareConsumer) {
3    consumer.apply(LoggerMiddleware).forRoutes('cats')
4  }
5}

路由守卫 Guard

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

  1. 创建 guard

    bash
    1nest generate guard
    2
    3? What name would you like to use for the guard? Roles
    4CREATE src/roles-guard/roles-guard.guard.spec.ts (185 bytes)
    5CREATE src/roles-guard/roles-guard.guard.ts (305 bytes)

    此时生成 guard 代码:

    ts
    1@Injectable()
    2export class RolesGuard implements CanActivate {
    3  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    4    return true
    5  }
    6}
  2. 通过@UseGuards 装饰器注入到 Controller 中

    ts
    1@Controller()
    2export class AppController {
    3  @Get()
    4  @UseGuards(RolesGuard)
    5  getHello(): string {
    6    return 'Hello World!'
    7  }
    8}

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

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

ts
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  app.useGlobalGuards(new RolesGuard())
4  await app.listen(3000)
5}
6bootstrap()

拦截器 Interceptor

Guard 可以抽离路由的访问控制逻辑,但是不能对请求、响应做修改,这种逻辑可以使用 Interceptor。

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

bash
1nest generate interceptor Route

生成代码

ts
1@Injectable()
2export class RouteInterceptor implements NestInterceptor {
3  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4    console.log('...before')
5
6    return next.handle().pipe(tap(() => console.log('...after')))
7  }
8}

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

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

在单个 Controller 中使用:

ts
1@Controller()
2export class AppController {
3  @Get()
4  @UseInterceptors(new RouteInterceptor())
5  getHello(): string {
6    return 'Hello World!'
7  }
8}

全局使用:

ts
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  app.useGlobalInterceptors(new RouteInterceptor())
4  await app.listen(3000)
5}
6bootstrap()

管道 Pipe

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

ts
1nest generate pipe App

生成代码:

ts
1@Injectable()
2export class AppPipe implements PipeTransform {
3  transform(value: any, metadata: ArgumentMetadata) {
4    return value
5  }
6}

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

内置的有 9 个 Pipe,从名字就能看出它们的意思:

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

同样,Pipe 可以只对某个参数生效:

tsx
1@Controller()
2export class AppController {
3  @Get(':id')
4  getHello(@Param('id', AppPipe) id: string): string {
5    console.log('——————🚀🚀🚀🚀🚀 —— id:', id)
6    return 'Hello World!'
7  }
8}

只对某个路由生效:

ts
1  @Post()
2  @UsePipes(AppPipe)
3  async create(@Body() createUser: CreateUserDto) {
4    // ...
5  }

global 生效:

ts
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  app.useGlobalPipes(new AppPipe())
4  await app.listen(3000)
5}
6bootstrap()

异常处理 ExceptionFilter

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

bash
1nest generate filter HttpException

生成代码

ts
1@Catch(HttpException)
2export class HttpExceptionFilter implements ExceptionFilter {
3  catch(exception: HttpException, host: ArgumentsHost) {
4    const ctx = host.switchToHttp()
5    const response = ctx.getResponse()
6    const request = ctx.getRequest()
7    const status = exception.getStatus()
8    response.status(status).json({
9      statusCode: status,
10      path: request.url,
11      message: exception.message,
12    })
13  }
14}

首先要实现 ExceptionFilter 接口,实现 catch 方法,就可以拦截异常了,但是要拦截什么异常还需要用 @Catch 装饰器来声明。

Nest 会 catch 所有未捕获异常,如果是 Exception Filter 声明的异常,那就会调用 filter 来处理。

拦截了异常之后,可以返回对应的响应,给用户更友好的提示。

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

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

也可以自己扩展,只需要继承自 HttpException 即可

ts
1export class IpRestriction extends HttpException {
2  constructor() {
3    super('Forbidden', 403)
4  }
5}

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

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

某个路由生效:

ts
1@Controller()
2export class AppController {
3  @Get()
4  @UseFilters(new HttpExceptionFilter())
5  getHello(): string {
6    return 'Hello World!'
7  }
8}

全局路由生效:

ts
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  app.useGlobalFilters(new HttpExceptionFilter())
4  await app.listen(3000)
5}
6bootstrap()

顺序

这里有一张别人总结好的图,根据需要在不同的切面里加入不同的逻辑即可。

img

总结

Nest 在传统 MVC 架构基础上,加入了 AOP 编程的思想,我们可以利用这些切面完成额外的某些逻辑,让 Controller 只专注于实现业务逻辑。

Nest 通过以下扩展点(顺序排放)实现 AOP:

  1. Middleware 中间件
  2. Guard 路由守卫,可以控制是否让 request 进入 handler 逻辑
  3. Interceptor 拦截器,可以做参数处理,修改 request 和 Response
  4. Pipe 管道:参数处理(转换、校验等)
  5. ExceptionFilter :错误处理