拦截器会用到RxJs
,所以在学习拦截器之前可以先了解一下它。
拦截器是使用@Injectable()
装饰器装饰的类并且实现了接口NestInterceptor
。
拦截器受到 AOP(面向切面编程)技术的启发,具有如下的功能:
- 在方法执行之前/之后绑定额外的逻辑
- 转换函数返回的结果
- 转换函数抛出的异常
- 扩展基本功能行为
- 根据特定条件完全覆盖函数
基础知识
每个拦截器都实现 Intercept() 方法,该方法采用两个参数。第一个参数是ExecutionContext
的实例,ExecutionContext
继承ArgumentsHost
。另外一个参数则是CallHandler
,在后面的小节中会介绍。
ExecutionContext
ExecutionContext
对象在守卫学习笔记已经做过简要的介绍,我们会在后续的笔记中进行详细说明。
CallHandler 接口说明
CallHandler
接口需要实现两个方法,一个是handle()
方法,这个方法可以在拦截器中某个时刻
调用路由处理程序的方法。另外一个方法是intercept()
方法,这个方法可以对请求/响应流
进行包装,因此可以在最终路由处理程序之前和之后实现自定义的逻辑。两个方法的关系是如果在 intercept() 方法的实现中不调用 handle()方法,则根本不会执行路由处理程序方法。
所以很明显,您可以在调用 handle()之前执行的 intercept()方法中编写代码,但是如何影响之后发生的事情呢?因为 handle()方法返回一个 Observable,所以我们可以使用强大的 RxJS 运算符来进一步操作响应。使用面向方面的编程术语,路由处理程序的调用(即调用 handle())称为切入点,表明这是插入我们的附加逻辑的点。
上面我们做了一些概述,我们现在结合例子来说明一下。在提供者笔记中我们有一个 POST /commodity/save
的请求接口,这个请求路径是在CommodityController
里面进行声明的。假如我们在这个请求路径上绑定拦截器的话,如果在此过程中的任何地方调用了不调用 handle()方法的拦截器,则 create()方法将不会被执行。一旦 handle()被调用(并且它的 Observable 已经返回),create()处理程序将被触发。一旦通过 Observable 接收到响应流,就可以对流执行附加操作,并将最终结果返回给调用者。
切面拦截
通过上述的介绍,我们来看看第一个用例,这个用例主要是使用拦截器来记录用户交互(存储用户调用、异步分派事件或计算时间戳)。具体实例代码如下:
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`)));}
}
说明:NestInterceptor<T, R>是一个通用的接口,其中 T 指示 an 的类型 Observable(支持响应流),R 是 所包装的值的类型 Observable。
由于 handle()返回 RxJS Observable,我们有多种操作符可供选择来操作流。在上面的示例中,我们使用了 tap()运算符,它在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会以其他方式干扰响应周期。
绑定拦截器
我们编写好拦截器后,可以使用@UseInterceptors()
来绑定拦截器。与管道和守卫一样,拦截器可以是控制器范围、方法范围或全局范围。
@Controller("commodity")
@UseInterceptors(LoggingInterceptor)
export class CommodityController {}
上述的例子中,我们的CommodityController
所有路由处理程序都将被LoggingInterceptor
进行拦截,当我们请求任何接口后,都可以在控制台看到如下的信息:
Before...
After... 2ms
请注意,我们传递了 LoggingInterceptor 类型(而不是实例),将实例化的责任留给框架并启用依赖项注入。与管道、防护和异常过滤器一样,我们也可以传递就地实例:
@Controller("commodity")
@UseInterceptors(new LoggingInterceptor())
export class CommodityController {}
全局拦截器设置可以如下:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
全局范围拦截器会应用于整个应用程序、每个控制器和每个路由。当我们需要在某个模块注入全局范围拦截器的依赖项是不行的,因为全局范围拦截器是在没有上下文环境下完成,所以没有上下文的支持,模块是不能找到对应实例。为了解决这个问题,我们可以用下面的方式来进行拦截器的注入:
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";@Module({providers: [{provide: APP_INTERCEPTOR,useClass: LoggingInterceptor,},],
})
export class AppModule {}
响应映射
我们已经知道 handle()返回一个 Observable. 该流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的 map()运算符轻松更改它。
让我们创建 TransformInterceptor,它将以一种简单的方式修改每个响应来演示该过程。它将使用 RxJS 的 map()运算符将响应对象分配给 data 新创建的对象的属性,并将新对象返回给客户端。具体实例如下:
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 })));}
}
// 路由调用的方法
findAll(): Commodity[] {return this.commoditys;
}
调用上述/commodity/all
请求地址后,我们可以看到如下的返回结果:
{"data": [{"id": 1,"username": "123","password": "123"},{"id": 2,"username": "456","password": "456"}]
}
说明:嵌套拦截器可以使用同步和异步 intercept()方法。async 如果需要, 您可以简单地切换该方法。
我们再举一个全局拦截器的例子,假如我们的请求参数可能会有带有 null 的参数,而 null 在程序处理时可能会出现异常,为此我们需要把 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)));}
}
异常拦截器例子
import {Injectable,NestInterceptor,ExecutionContext,BadGatewayException,CallHandler,
} from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";@Injectable()
export class ErrorsInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {return next.handle().pipe(catchError((err) => throwError(() => new BadGatewayException())));}
}
流覆盖
有几个原因导致我们有时可能希望完全阻止调用处理程序并返回不同的值。一个明显的例子是实现缓存以提高响应时间。让我们看一下一个简单的缓存拦截器,它从缓存返回其响应。在实际示例中,我们需要考虑其他因素,例如 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()路由处理程序。当有人调用使用 的端点时,将立即返回响应(硬编码的空数组)。为了创建通用解决方案,您可以利用并创建自定义装饰器。后续的笔记中会举出一些更详细的例子。