1. 基本使用
Nest的拦截器是在函数执行之前/之后绑定额外的逻辑,类似于axios的请求拦截器和响应拦截器。
1. 使用命令创建一个拦截器
nest g interceptor core/interceptor/global --no-spec --flat
将会在core/interceptor下生成global.interceptor.ts
2. 定义拦截器,修改global.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('global Before...');
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`global After... ${Date.now() - now}ms`)));
}
}
拦截器是使用 @Injectable() 装饰器注解的类。它接收2个参数: 第一个是 ExecutionContext 实例,即执行上下文;第二个参数是 CallHandler,即将被执行的方法具柄(指向)。
rxjs:是一个基于观察者模式的响应式编程的库,主要用于处理异步数据流。
next.handle():调用下一个处理程序,并返回一个 Observable。这个 Observable 在处理程序完成后将发出一个值,通常是响应数据。
使用管道操作符.pipe,对响应后的数据进行一系列处理操作。tap 操作符每次输入值后接收,但不改变值,即next.handle()发出一个值,tap监听到事件,触发回调函数。这类似于webpack的事件流框架Tapable使用 tap 注册,通过 call 触发,都是发布订阅模式。
2. rxjs操作符
在使用拦截器前,先介绍下rxjs库的几个操作符:
map:可以修改输入的值,再输出。类似于JS的map函数;
tap:每次输入值后接收,但不改变值;
timeout:设置一个过期时间,当这个时间段内没有接收到完成信号就会出发这个逻辑;
catchError:当管道中发生异常(throw)后,执行的操作;
of:创建一个新的stream流(返回新的结果)
3. 注册
1. 全局注册
在上文中,创建了global.interceptor.ts拦截器,现注册到全局中,修改main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalInterceptor } from 'src/core/interceptor/global.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
// 注册全局拦截器
app.useGlobalInterceptors(new GlobalInterceptor());
await app.listen(3000);
}
bootstrap();
启动程序后,访问localhost:3000,控制台将打印
global Before...
global After... 1ms
2. 类注册
1. 使用命令创建一个拦截器
nest g interceptor core/interceptor/class --no-spec --flat
2. 修改class.interceptor.ts文件,内容与global.interceptor.ts类似,只是打印值不同
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class ClassInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('class Before...');
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`class After... ${Date.now() - now}ms`)));
}
}
3. 注册,修改app.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ClassInterceptor } from 'src/core/interceptor/class.interceptor';
@Controller()
@UseInterceptors(ClassInterceptor)
export class AppController {
@Get()
getHello(): string {
return 'hello';
}
}
3. 方法注册
1. 使用命令创建一个拦截器
nest g interceptor core/interceptor/method --no-spec --flat
2. 修改method.interceptor.ts文件,内容依旧与global.interceptor.ts类似,只是打印值不同
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class MethodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('method Before...');
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`method After... ${Date.now() - now}ms`)));
}
}
3. 注册,修改app.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ClassInterceptor } from 'src/core/interceptor/class.interceptor';
import { MethodInterceptor } from 'src/core/interceptor/method.interceptor';
@Controller()
@UseInterceptors(ClassInterceptor)
export class AppController {
@Get()
@UseInterceptors(MethodInterceptor)
getHello(): string {
return 'hello';
}
}
4. 多个拦截器执行顺序
分别注册了全局拦截器,类拦截器,方法拦截器。启动程序,访问localhost:3000,控制台打印:
global Before...
class Before...
method Before...
method After... 0ms
class After... 1ms
global After... 3ms
从控制台的打印结果可以看出,拦截器是符合洋葱模型的,这里放一张洋葱模型图片
5. 全局注册,拦截成功的返回数据
一般开发中是不会根据HTTP状态码来判断接口成功与失败的,而是会根据请求返回的数据,里面加上code字段。例如非VIP用户访问VIP的内容时,状态码依旧返回200,代表接口请求成功,但是返回的数据中,code为-1,说明无权限访问该模块。
1. 修改全局拦截器global.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 0,
msg: '请求成功',
};
}),
);
}
}
tap 操作符每次输入值后接收,但不改变值。这里的map操作符可以修改输入的值,再输出。类似于JS的map函数;
2. 之前以及在main.ts中注册过,这里不再注册。
启动程序,访问localhost:3000,接口返回:
{
"data": "hello",
"code": 0,
"msg": "请求成功"
}
6. 方法注册,超时捕获
对于一些特殊的,请求时间较长的接口,设置一个超时时间,当接口超时,主动抛出异常
1. 修改method.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import {
Observable,
timeout,
catchError,
of,
throwError,
TimeoutError,
} from 'rxjs';
@Injectable()
export class MethodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(1000),
catchError((err) => {
if (err instanceof TimeoutError) {
return of('Timeout Infomation');
}
return throwError(() => err);
}),
);
}
}
timeout:设置一个过期时间,当这个时间段内没有接收到完成信号就会出发这个逻辑;
catchError:当管道中发生异常(throw)后,执行的操作;
of:创建一个新的stream流(返回新的结果)
2. 修改app.controller.ts
将接口设为异步,延迟10s再响应
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ClassInterceptor } from 'src/core/interceptor/class.interceptor';
import { MethodInterceptor } from 'src/core/interceptor/method.interceptor';
function sleep() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('');
}, 10000);
});
}
@Controller()
@UseInterceptors(ClassInterceptor)
export class AppController {
@Get()
@UseInterceptors(MethodInterceptor)
async getHello() {
await sleep();
return 'hello';
}
}
启动程序,使用Postman测试,访问localhost:3000,1s后接口返回“Timeout Infomation”,说明已成功被catchError捕获。
控制器输出:
class Before...
method Before...
而没有输出method After,class After,是因为使用了of创建了一个新的stream流,不走接下来的控制器操作了。
结尾
往前相关Nest文章推荐:
后续会继续更新Nest系列文章,感兴趣的可先关注我。