拦截器interceptors需要被@Injectable()装饰并且实现NestInterceptor接口
拦截器具有一组有用的功能,这些功能受到面向切面编程(AOP)技术的启发。它们可以:
- 在方法执行前后绑定额外的逻辑
- 转换返回给方法结果
- 转换从方法抛出的异常
- 扩展一个方法行为
- 基于一些特定的条件,完全覆盖一个方法(例如缓存)
基本
每个拦截器都实现了带有2个参数的intercept()方法。第一个参数是ExecutionContext实例(和守卫一样)。ExecutionContext继承自ArgumentsHost。我们在之前的exception filters章节已经见过这位老朋友了。那一章介绍了ArgumentsHost的一些功能和作用,想了解的小伙伴们可以回到那一章再看下。
Execution context
伴随着ArgumentsHost的扩展,ExecutionContext还增加了几个新的帮助方法在当前线程中提供了附加的情报。这些情报对我们来说是非常有用的。想知道更多情报,请参照ExecutionContext
Call handler
第二个参数是CallHandler。CallHandler接口实现了handle()方法,您可以使用它在拦截器的某个点调用路由处理程序方法。如果您在接口的intercept()方法中不调用handle()方法,那么这个路由将不会执行。
这种方法说明intercept()有效的包装了请求/响应流。因此,您可以在路由执行的前后实现自己的逻辑。这就清楚了您可以在intercept()方法里handle()执行前撸自己的代码,但是这么影响后面呢?因为handle()方法返回Observable,所以我们可以使用强大的RxJS来操作将来的返回。使用面向切面编程,路由处理程序的调用(即,调用handle())被称为Pointcut,表示它是插入附加逻辑的点。
例如POST/cats请求,这个请求的目的是在CatsController里面的create()方法。如果拦截器在任何地方都不调用handle()方法,create()方法将不会被执行。一旦handle()被调用(并且它的Observalbe已经返回),create()方法将会被触发。一旦通过Observable接收到响应流,就可以对该流执行其他操作,并将最终结果返回给调用者。
拦截层
第一个作用就是用来打印用户的操作行为(计算时间戳,存储用户信息等等)
LogginInterceprtor简单举例
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
注意:
拦截器,和控制器,守卫等等一样,可以通过contructor构造器来进行依赖注入
从handle()返回RxJS的Observable开始,我们有很多操作流的选择。在接下来的例子中,我们使用了tap()运算符,它在可观察流优雅或异常终止时调用匿名日志记录函数,但不会干扰响应周期。
绑定拦截器
为了设定拦截器,我们使用从@nestjs/common包导出的@UseInterceptors()装饰器。和管道还有守卫一样,拦截器可以装饰控制器,方法,或者全局的。
@UseInterceptors(LoggingInterceptor)
export class CatsController {
constructor(private readonly catsService: CatsService) {}
}
使用上面的构造,每个定义在CatsController里面的路由都会调用LoggingInterceptor。当调用其中的某个请求,可以看到下面的消息:
Before...
After... 1ms
注意到,我传入的是一个类,实际的创建是由Nest通过依赖注入创建的。和管道,守卫,异常过滤器一样,我们也可以创建一个实例
@UseInterceptors(new LoggingInterceptor())
export class CatsController {
constructor(private readonly catsService: CatsService) {}
}
像我们提到的那样,上面的拦截器将对每个在这个控制器里面的方法有效,如果只想要在某个方法上进行拦截,需要在那个方法上面添加拦截器。
为了设定一个全局的拦截器,在Nest程序创建的时候使用useGlobalInterceptors()
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor())
和其他的一样,使用useGlobal这种方法后,拦截器内部就不能使用框架的依赖注入了,因为没有在模块内进行注册。如果想要解决这个问题,我们可以直接在模块内进行注册
import { LoggingInterceptor } from 'src/logging/logging.interceptor';
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [ {
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor
}],
})
export class AppModule {}
当使用这种方法为拦截器执行依赖注入时,请注意,无论使用这种构造的模块是什么,拦截器实际上都是全局的。这应该在哪里进行?选择定义拦截器的模块(上面示例中的LoggingInterceptor)。此外,useClass不是处理自定义提供程序注册的唯一方法。在这里(https://docs.nestjs.com/fundamentals/custom-providers)了解更多信息。
响应映射
我们已经知道handle()返回Observable。这个流包含了从路由来的返回值,所以我们能很方便的使用RxJS的map()来操作它。
警告:
响应映射功能不适用于库特定的响应策略(禁止直接使用@Res()对象)。
让我们创建TransformInterceptor,他会改变每个返回结果。它将使用RxJS的map()运算符将响应对象分配给新创建的对象的数据属性,并将新对象返回给客户端。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
嵌套拦截器同时使用同步和异步interceptor()方法。如果需要,您可以简单地将方法切换为异步。
使用上述构造,当有人调用GET/cats端点时,响应如下(假设路由处理程序返回一个空数组[]):
{
"data": []
}
拦截器在为整个应用程序中出现的需求创建可重用的解决方案方面具有巨大价值。例如,假设我们需要将每次出现的null转换为空字符串''。我们可以使用一行代码并全局绑定拦截器,以便每个注册的处理程序自动使用它。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
异常映射
另一个有趣的用例是利用RxJS的catchError()运算符重写抛出的异常:
覆盖流
有时我们可能希望完全阻止调用处理程序并返回不同的值,这有几个原因。一个明显的例子是实现缓存以提高响应时间。让我们来看一个简单的缓存拦截器,它从缓存返回响应。在一个现实的例子中,我们希望考虑其他因素,如TTL、缓存无效、缓存大小等,但这超出了本文讨论的范围。在这里,我们将提供一个基本的示例来演示主要概念。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
我们的CacheInterceptor有一个硬编码的isCached变量和一个硬代码的响应[]。需要注意的关键点是,我们在这里返回一个由RxJS of()运算符创建的新流,因此根本不会调用路由处理程序。当有人调用使用CacheInterceptor的端点时,将立即返回响应(硬编码的空数组)。为了创建通用解决方案,您可以利用Reflector并创建自定义装饰器。反射在守卫章节中有很好的描述。
更多操作
使用RxJS运算符操纵流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。假设您希望处理路由请求的超时。如果端点在一段时间后没有返回任何内容,则希望以错误响应终止。以下结构实现了这一点:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
5秒后,请求处理将被取消。您还可以在抛出RequestTimeoutException之前添加自定义逻辑(例如发布资源)。