Nest 中的拦截器(Interceptor)用于在请求处理函数之前和之后进行逻辑处理。有点类似于 axios 中的请求拦截器和响应拦截器。常见的应用场景有转换返回结果、缓存拦截器、超时拦截器等。
Nest 中拦截器的核心逻辑(对返回数据的流式操作)依赖于 RxJS 库,所以我们先简单介绍一下 RxJS。
RxJS
RxJS 是一个用于处理异步数据流的库,它提供了很多操作符,用于简化异步逻辑的编写。
核心概念
先来了解 RxJS 的几个核心概念:
Observable 可观察对象
表示一个随着时间推移可以发出多个值的数据流。
-
使用
new Observable
可以创建一个 Observable 对象。 -
new Observable
回调函数中的逻辑为 Observable 执行逻辑,它是惰性运算的,只有在观察者订阅后才会执行。 -
Observable 执行可以推送三种类型的通知:
next()
发送数据error()
发送错误complete()
结束数据流error()
和complete()
只能在 Observable 执行期间发生一次,且只会执行其中一个。
import { Observable } from 'rxjs';
const observable = new Observable(observer => {
observer.next('Hello');
observer.next('World');
observer.complete();
});
Observer 观察者
监听 Observable 发出的值。
-
Observer 是一组回调的集合,每个回调函数对应一种 Observable 推送的通知类型。
-
要使用观察者,需要把它传递给 Observable 对象的
subsrcibe
方法。订阅建立起 Observable 和 Observer 之间的联系,订阅后观察者可以接收 Observable 流发出的数据。
const observer = {
next: value => console.log('Next:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Complete!')
};
observable.subscribe(observer);
// 输出
// Next: Hello
// Next: World
// Complete!
Operators 操作符
RxJS 提供了大量的操作符来转换、过滤、组合、合并数据流。即由数据源(Observable)产生的数据,经过一系列 Operators 处理,最后传给 Observer。这些操作符在 Nest 拦截器对返回数据的处理中也起着重要的作用。
常见操作符
下面介绍一些 RxJS 中常见的操作符。
创建操作符
of
创建包含特定值的 Observable 流。
import { of } from 'rxjs';
const observable = of(1, 2, 3);
from
将数组、promise、可迭代对象转为 Observable 流。
import { from } from 'rxjs';
const observable = from([1, 2, 3]);
转换操作符
map
对每个值进行转换。
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
of(1, 2, 3).pipe(
map(x => x * 2)
).subscribe(console.log); // 2 4 6
scan
类似 reduce,累计值并发出中间结果。
import { of } from 'rxjs';
import { scan } from 'rxjs/operators';
of(1, 2, 3).pipe(
scan((acc, x) => acc + x, 0)
).subscribe(console.log); // 1 3 6
错误处理操作符
catchError
捕获发出的错误并返回新的 Observable。
import { of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
const source = throwError(() => new Error('Something went wrong!')); // 模拟错误
source
.pipe(
catchError(err => {
console.error('Error caught:', err.message);
return of('Fallback value'); // 返回一个新的流作为替代
})
)
.subscribe({
next: value => console.log('Next:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Complete!'),
});
// 输出:
// Error caught: Something went wrong!
// Next: Fallback value
// Complete!
工具操作符
tap
对每个值执行副作用,但不会修改流的值。
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
of(1, 2, 3)
.pipe(
tap(value => console.log(`Value passed through: ${value}`)), // 记录值
)
.subscribe(value => console.log('Subscriber received:', value));
// 输出:
// Value passed through: 1
// Subscriber received: 1
// Value passed through: 2
// Subscriber received: 2
// Value passed through: 3
// Subscriber received: 3
timeout
在超时时间内未发出值时抛出错误。
import { interval } from 'rxjs';
import { timeout } from 'rxjs/operators';
interval(3000) // 每 3 秒发出一个值
.pipe(timeout(2000)) // 超时设置为 2 秒
.subscribe({
next: value => console.log('Next:', value),
error: err => console.error('Error:', err.message),
});
// 输出:
// Error: Timeout has occurred
创建拦截器
下面我们来尝试创建一个 Nest 拦截器。
nest cli 创建拦截器
nest g interceptor interceptor/transform
执行这行命令后,在 src/interceptor/transform 文件夹中创建 transform.interceptors.ts 文件:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
可以看出拦截器是具有 @Injectable()
装饰器的类,即声明这个类被 Nest IoC 容器接管,可以通过 constructor
注入依赖。同时,它需要实现 NestInterceptor
接口,这个接口有一个 intercept
方法。
实现 intercept 方法
参数
intercept
方法接收两个参数:
context
:一个ExecutionContext
对象,与【Nest指北系列】守卫中的canActive
方法的参数相同。有getClass
和getHandler
两个方法,可以配合Reflector
获取元数据。next
: 接口定义如下,next.handle()
函数即路由处理函数,返回值为 Observable 对象包装的路由处理函数的返回值。后续可以通过 RxJS 操作符对这个 Observable 数据流进行处理,后续的应用场景一节中有具体的示例。
export interface CallHandler<T = any> { handle(): Observable<T>; }
比如:访问 /user 时,路由处理函数返回 []
,在应用拦截器的情况下,调用 next.handle()
方法的返回值就是 Observable<[]>
对象。
@Controller('user')
export class UserController {
@Get()
list() {
return [];
}
}
注意:需要在拦截器中调用 next.handle() 方法才会执行对应路由处理函数,不调用就不会执行。
使用拦截器
拦截器分为:方法拦截器、控制器拦截器、全局拦截器,其中方法拦截器和控制器拦截器都适用 @UseInterceptors()
装饰器绑定。
@UseInterceptors 装饰器
- 可以传入实例;也可以直接传入类,将实例化的过程交给 Nest 框架进行。更推荐使用类的方式,这样可以在多个模块中复用同一个实例。
@UseInterceptors(TransformInterceptor)
export class UserController {}
@UseInterceptors(new TransformInterceptor())
export class UserController {}
控制器拦截器
@UseInterceptors(TransformInterceptor)
export class UserController {}
路由方法拦截器
export class UserController {
@Get('')
@UseInterceptors(TransformInterceptor)
getUsers() {}
}
全局拦截器
通过 app.useGlobalInterceptors
绑定
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
执行顺序
拦截器的执行顺序类似洋葱模型,如果 TransformInterceptor 中在进入请求前和返回响应后分别打 log,执行顺序如下:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before');
return next.handle().pipe(
tap(() => console.log('After'))
);
}
}
global Before
class Before
method Before
method After
class After
global After
拦截器应用场景
拦截器配合 RxJS 运算符可以实现多个应用场景。
在路由处理程序前后添加额外逻辑
实现计算路由处理程序执行时间并输出:
- 由于执行顺序为 拦截器 -> 路由处理程序 -> 拦截器,所以可以通过前后打点计算路由执行时间。
- tap 运算符:对 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 controller = context.getClass(); // 获取当前调用的控制器
const handler = context.getHandler(); // 获取当前调用的处理器
// 保存路由执行前的时间
const now = Date.now();
return next
.handle()
.pipe(
// 计算出这个路由的执行时间
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
对比中间件
-
中间件只能在路由处理程序之前添加额外逻辑,而拦截器可以在之前和之后分别添加逻辑,且拦截器可以使用 RxJS 操作符对响应流进行处理。
-
拦截器可以从 context 拿到目标的 class 和 handler,进而配合 Reflector 获取元信息,而中间件不行。
转换路由处理程序的返回结果
实现统一的响应返回:
- map 运算符:对响应结果 Observable 流的每个值进行转换,得到标准输出格式。
import { Injectable, NestInterceptor, CallHandler } from '@nestjs/common'
import { map } from 'rxjs/operators'
import {Observable} from 'rxjs'
interface data<T>{
data:T
}
@Injectable()
export class Response<T = any> implements NestInterceptor {
intercept(context, next: CallHandler):Observable<data<T>> {
return next.handle().pipe(map(data => {
return {
data,
status:0,
success:true,
message:"成功"
}
}))
}
}
转换路由处理程序抛出的异常
- catchError 运算符:捕获错误并进行转换。
import { EntityNoFoundException } from '@common/exception/common.exception'
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable, throwError } from 'rxjs'
import { catchError } from 'rxjs/operators'
@Injectable()
export class TypeOrmExceptionInterceptor implements NestInterceptor {
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(err => {
if (err.name === 'EntityNotFound') {
return throwError(new EntityNoFoundException())
}
return throwError(err)
}),
)
}
}
扩展路由处理程序的行为
实现在用户调用某些接口后,对用户执行一些额外操作,比如添加角色:
- tap 运算符:在接口调用后执行副作用,给用户绑定上角色。
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { User } from '@src/auth/user/user.entity'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
import { getConnection } from 'typeorm'
@Injectable()
export class BindRoleToUserInterceptor implements NestInterceptor {
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
tap(async () => {
const req = context.switchToHttp().getRequest()
await this.bindRoleToUser(req.roleId, req.user.id)
}),
)
}
}
根据条件重写路由处理程序
实现缓存拦截器:
- 当命中缓存时,通过 of 运算符创建新的 Observable 流并返回,而不调用
next.handle()
,即不走原来的路由处理程序。
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();
}
}
超时拦截器
- timeout 操作符:在特定时间内没收到消息的时候抛出
TimeoutError
错误。 - catchError 操作符:捕获错误,判断如果是
TimeoutError
,就返回RequestTimeoutException
,有内置的异常处理器对应这个异常,会返回标准的响应格式。
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);
}),
);
};
};
总结
本章介绍了 Nest 中的拦截器,包括它的创建和使用。自定义拦截器时需要实现 NestInterceptor
类的 intercept
方法,其 next
参数的 handle
方法返回 RxJS Observable 对象,即路由处理函数的返回值,对其进行操作时需要用到 RxJS 库中的各种操作符。