Skip to content

拦截器

拦截器与中间件一致,遵循 AOP 的思想实现,但是它们的作用却不相同。

似乎中间件能做的事,拦截器也能做:在中间件中可以获取 RequestObject 对象,在拦截器中也能通过 ExecutionContext 获取,那是不是拦截器就能完成中间件、拦截器的工作了呢?

对比了它们摘自官网的介绍,能发现这种理解是错误的。

中间件介绍:

Middleware functions can perform the following tasks:

  • execute any code.
  • make changes to the request and the response objects.
  • end the request-response cycle.
  • call the next middleware function in the stack.
  • if the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

拦截器介绍:

Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:

  • bind extra logic before / after method execution
  • transform the result returned from a function
  • transform the exception thrown from a function
  • extend the basic function behavior
  • completely override a function depending on specific conditions (e.g., for caching purposes)
  • 中间件和拦截器都能在请求到达前完成逻辑,但中间件的执行时机要早于拦截器。
  • 拦截器的侧重点在 AOP ,它的执行时机是在路由方法的前后调用。
  • 中间件可以中断请求,拦截器不行。
  • 中间件无法知道当前处理请求的控制器和路由方法,拦截器可以,因为它依赖于控制器方法前后调用的。

使用拦截器

拦截器是用 @Injectable() 装饰的类,需要实现 NestInterceptor 接口。它的表现得与中间件一致,能在方法前后做一些函数的增强,例如它能实现下面的功能:

  • 在方法执行前后绑定额外的逻辑
  • 转换返回给方法结果
  • 转换从方法抛出的异常
  • 扩展一个方法行为
  • 基于一些特定的条件,完全覆盖一个方法(例如缓存)

我们接下来定义一个简单的拦截器:

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

export class LoggerInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> | Promise<Observable<any>> {
    console.log("Before...");
    const now = Date.now();
    return next.handle().pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
  }
}

绑定注册到控制器上:

ts
import { Controller, Get, UseInterceptors } from "@nestjs/common";
import { AppService } from "./app.service";
import { LoggerInterceptor } from "./interceptors/logger.interceptor";

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseInterceptors(LoggerInterceptor)
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

发送请求,获得如图的结果:

image-20240122223330988

观察上面的例子,可以发现以下几点:

  • 必须返回一个 RxJS 响应流(包含 Promise 类型),否则接口类型不符会抛出错误。
  • next 包含一个 handle() 方法,调用该方法会返回一个 RxJS Observable 响应流,调用 handle() 会执行路由方法,这在 AOP 的术语被称为 切入点
  • 拦截器有两个执行时机,时间线以 next.handle() 方法作为界线,带有打印 Before 的部分为进入路由方法之前被调用,调用 handle() 后的返回的 RxJS Observable 响应流所执行的逻辑。
  • 传递一个类似 ArgurmentHost 的参数,ExecutionContext 继承自它,额外提供了当前执行过程的其他详细信息,比如获取当前增强方法的反射数据(当前处理方法以及包含它的控制器类)。

ExecutionContext 与 ArgumentsHost

更多 ExecutionContextArgumentsHost 请到查阅官方文档

绑定位置

与其他的组件一样,拦截器能在局部、全局进行绑定。

局部:

  • 控制器
  • 路由方法
ts
// 1.绑定控制器
@UseInterceptors(LoggerInterceptor)
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  // 2.绑定路由方法
  @UseInterceptors(LoggerInterceptor)
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

全局:

  • 主入口全局绑定,使用 app.useGlobalInterceptors
  • 根模块依赖注入
ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 1.主入口全局绑定
  app.useGlobalInterceptors(new LoggerInterceptor());
  await app.listen(3000);
}
bootstrap();

// ---------------------------------------------

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    // 2.根模块依赖注入绑定
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggerInterceptor
    }
  ]
})
export class AppModule {}

多重拦截器

同一位置多拦截器

当有多个拦截器绑定在同一个位置时,它们的顺序是固定的,会根据洋葱模型的特性,执行结果跟回形标类似,根据定义的顺序从下往上执行,然后回过头从上往下执行,比如下面这样:

ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  // logger1与 logger2在打印上稍有不同
  @UseInterceptors(LoggerInterceptor2)
  @UseInterceptors(LoggerInterceptor)
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

结果如下图:

image-20240122225329006

不同位置多个拦截器

不同位置存在多个拦截器,比如全局、控制器、路由方法都存在拦截器时,它们的执行顺序是正序的,也就是:全局 > 控制器 > 方法

ts
// 全局 
app.useGlobalInterceptors(new LoggerInterceptor3());

// 控制器
@UseInterceptors(LoggerInterceptor2)
@Controller()
export class AppController {
  // ...
}

// 方法
@UseInterceptors(LoggerInterceptor)
@Get()
getHello(): string {
  // ...
}

结果如下图:

image-20240122225918329

应用场景

基于 AOP 思想实现的组件,列举以下常用场景:

  • 日志记录。
  • 封装统一的响应结果。
  • 通过 RxJS 超时异常处理。
  • 缓存拦截,命中缓存直接返回,未命中可存入缓存。
  • 请求预处理,在请求未到达路由之前,对请求进行验证、修改请求头。
  • 数据加密和解密。
  • 流量控制,限制请求的频率。
  • 事件订阅。
  • ...

用过 Axios 网络请求库的同学在阅读本章节时,可能会有一种感觉:拦截器好像 Axios 的请求拦截和响应拦截。事实也确实如你所感觉的一样,拦截器的两段执行时机,对应着请求拦截和响应拦截。

前端开发通常在拦截器中做的最多的:

  • 请求预处理,在发出请求前,添加上 token 等请求头。
  • 提取响应结果,封装服务器响应结果为需要的数据格式,以便后续处理。

案例一:响应拦截,统一返回

案例二:超时处理

案例三:缓存

如有转载或 CV 的请标注本站原文地址