拦截器
拦截器与中间件一致,遵循 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
接口。它的表现得与中间件一致,能在方法前后做一些函数的增强,例如它能实现下面的功能:
- 在方法执行前后绑定额外的逻辑
- 转换返回给方法结果
- 转换从方法抛出的异常
- 扩展一个方法行为
- 基于一些特定的条件,完全覆盖一个方法(例如缓存)
我们接下来定义一个简单的拦截器:
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`)));
}
}
绑定注册到控制器上:
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();
}
}
发送请求,获得如图的结果:
观察上面的例子,可以发现以下几点:
- 必须返回一个
RxJS
响应流(包含Promise
类型),否则接口类型不符会抛出错误。 next
包含一个handle()
方法,调用该方法会返回一个RxJS Observable
响应流,调用handle()
会执行路由方法,这在AOP
的术语被称为 切入点。- 拦截器有两个执行时机,时间线以
next.handle()
方法作为界线,带有打印Before
的部分为进入路由方法之前被调用,调用handle()
后的返回的RxJS Observable
响应流所执行的逻辑。 - 传递一个类似
ArgurmentHost
的参数,ExecutionContext
继承自它,额外提供了当前执行过程的其他详细信息,比如获取当前增强方法的反射数据(当前处理方法以及包含它的控制器类)。
ExecutionContext 与 ArgumentsHost
更多 ExecutionContext
与 ArgumentsHost
请到查阅官方文档。
绑定位置
与其他的组件一样,拦截器能在局部、全局进行绑定。
局部:
- 控制器
- 路由方法
// 1.绑定控制器
@UseInterceptors(LoggerInterceptor)
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// 2.绑定路由方法
@UseInterceptors(LoggerInterceptor)
@Get()
getHello(): string {
return this.appService.getHello();
}
}
全局:
- 主入口全局绑定,使用
app.useGlobalInterceptors
- 根模块依赖注入
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 {}
多重拦截器
同一位置多拦截器
当有多个拦截器绑定在同一个位置时,它们的顺序是固定的,会根据洋葱模型的特性,执行结果跟回形标类似,根据定义的顺序从下往上执行,然后回过头从上往下执行,比如下面这样:
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// logger1与 logger2在打印上稍有不同
@UseInterceptors(LoggerInterceptor2)
@UseInterceptors(LoggerInterceptor)
@Get()
getHello(): string {
return this.appService.getHello();
}
}
结果如下图:
不同位置多个拦截器
不同位置存在多个拦截器,比如全局、控制器、路由方法都存在拦截器时,它们的执行顺序是正序的,也就是:全局 > 控制器 > 方法
。
// 全局
app.useGlobalInterceptors(new LoggerInterceptor3());
// 控制器
@UseInterceptors(LoggerInterceptor2)
@Controller()
export class AppController {
// ...
}
// 方法
@UseInterceptors(LoggerInterceptor)
@Get()
getHello(): string {
// ...
}
结果如下图:
应用场景
基于 AOP
思想实现的组件,列举以下常用场景:
- 日志记录。
- 封装统一的响应结果。
- 通过
RxJS
超时异常处理。 - 缓存拦截,命中缓存直接返回,未命中可存入缓存。
- 请求预处理,在请求未到达路由之前,对请求进行验证、修改请求头。
- 数据加密和解密。
- 流量控制,限制请求的频率。
- 事件订阅。
- ...
用过 Axios
网络请求库的同学在阅读本章节时,可能会有一种感觉:拦截器好像 Axios
的请求拦截和响应拦截。事实也确实如你所感觉的一样,拦截器的两段执行时机,对应着请求拦截和响应拦截。
前端开发通常在拦截器中做的最多的:
- 请求预处理,在发出请求前,添加上
token
等请求头。 - 提取响应结果,封装服务器响应结果为需要的数据格式,以便后续处理。