1.自定义拦截器

我们可以把拦截器理解为函数增强器,它能对目标函数进行增强,拦截器受AOP(面向切面编程)思想启发,可以在目标函数之前或之后进行绑定额外的逻辑。拦截器可以实现对目标方法返回结果的转换,也可以实现对目标函数的异常处理转换,拦截器可以扩展目标函数。

自定义拦截器需要满足如下两个条件:
(1).拦截器需要使用@Injectable()装饰器装饰。
(2).拦截器类需要实现NestInterceptor接口,重写intercept()函数,此函数接收两个参数,参数1是一个ExecutionContext对象,ExecutionContext继承自ArgumentsHost对象,ExecutionContext还提供了其他功能,通过ExecutionContext兑现可以获取请求或响应对象。intercept()函数的第二个参数是一个CallHandler对象,CallHandler是一个包装执行流的对象,如果不调用CallHandler对象的handler()函数,那么就不会执行拦截的目标函数。intercept()函数返回一个Observable对象,Observable对象由rxjs导出,Observable对象提供了一组非常强大的运算符,可以帮助我们进行例如响应操作。
NestInterceptor 是一个通用接口,其中 T 表示已处理的 Observable 的类型(在流后面),而 R 表示包含在返回的 Observable 中的值的返回类型。

  1. // src/common/interceptors/logging.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. ExecutionContext,
  6. CallHandler,
  7. } from '@nestjs/common';
  8. import { Observable } from 'rxjs';
  9. import { tap } from 'rxjs/operators';
  10. /**
  11. * 我们可以把拦截器理解为函数增强器,它能对目标函数进行增强,拦截器受AOP(面向切面编程)思想启发,
  12. * 可以在目标函数之前或之后进行绑定额外的逻辑。拦截器可以实现对目标方法返回结果的转换,也可以实现对目标
  13. * 函数的异常处理转换,拦截器可以扩展目标函数。
  14. *
  15. * 自定义拦截器需要满足以下两个条件:
  16. * (1).需要被@Injectable()装饰器装饰。
  17. * (2).需要实现NestInterceptor接口,重写intercept()函数,intercept()函数接收两个参数,
  18. * 第一个参数是ExecutionContext执行上下文对象,ExecutionContext继承自ArgumentsHost对象,并提供了更多扩展功能,
  19. * 第二参数是一个CallHandler对象,CallHandler是一个包装执行流的对象,如果不调用CallHandler对象的handler()函数,
  20. * 那么就不会执行拦截的目标函数。intercept()函数返回一个Observable对象,Observable对象由rxjs导出,
  21. * Observable对象提供了一组非常强大的运算符,可以帮助我们进行例如响应操作。
  22. *
  23. * NestInterceptor<T,R> 是一个通用接口,其中 T 表示已处理的 Observable<T> 的类型(在流后面)
  24. * ,而 R 表示包含在返回的 Observable<R> 中的值的返回类型。
  25. */
  26. @Injectable()
  27. export class LoggingInterceptor implements NestInterceptor {
  28. intercept(
  29. context: ExecutionContext,
  30. next: CallHandler<any>,
  31. ): Observable<any> | Promise<Observable<any>> {
  32. console.log('在拦截器拦截目标方法前执行...');
  33. const now = Date.now();
  34. return next.handle().pipe(
  35. tap(() => {
  36. console.log(`在拦截器拦截目标方法后执行...,耗时:${Date.now() - now}ms`);
  37. }),
  38. );
  39. }
  40. }

1.1 局部使用自定义拦截器

我们可以从@nestjs/common中导出@UseInterceptors()装饰器来指定使用的拦截器,@UseInterceptors()可以用在路由方法上,那么只会作用于带有@UseInterceptors()装饰器的路由方法,@UseInterceptors()也可以放在控制器上,@UseInterceptors()会作用与控制器下所有路由方法。@UseInterceptors()可以传入一个类或类的实例,传入类例如@UseInterceptors(LoggingInterceptor),这样做的好处是利用框架承担类的实例化并启用依赖注入,当然你可以传入类的实例,例如:@UseInterceptors(new LoggingInterceptor()),推荐传入类而不是类的实例。

  1. import { Controller, Get, UseInterceptors } from '@nestjs/common';
  2. import { AppService } from './app.service';
  3. import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
  4. @Controller()
  5. export class AppController {
  6. constructor(private readonly appService: AppService) {}
  7. @Get('/hello')
  8. @UseInterceptors(LoggingInterceptor)
  9. hello(): string {
  10. return 'hello';
  11. }
  12. }

1.2 全局使用自定义拦截器

设置全局拦截器:

  1. // src/main.ts
  2. import { NestFactory } from '@nestjs/core';
  3. import { AppModule } from './app.module';
  4. import {LoggingInterceptor} from './common/interceptors/logging.interceptor'
  5. async function bootstrap() {
  6. const app = await NestFactory.create(AppModule);
  7. app.useGlobalInterceptors(new LoggingInterceptor())
  8. app.listen(3000);
  9. }
  10. bootstrap();

全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局拦截器无法插入依赖项, 因为它们不属于任何模块。为了解决此问题, 您可以使用以下构造直接从任何模块设置一个拦截器,在根模块设置拦截器,以便其他模块使用注入拦截器:

  1. // src/app.module.ts
  2. import { Module } from '@nestjs/common';
  3. import { APP_INTERCEPTOR } from '@nestjs/core';
  4. import {LoggingInterceptor} from './common/interceptors/logging.interceptor'
  5. @Module({
  6. providers: [
  7. {
  8. provide: APP_INTERCEPTOR,
  9. useClass: LoggingInterceptor,
  10. },
  11. ],
  12. })
  13. export class AppModule {}

浏览器访问localhost:3000/hello测试自定义拦截器是否生效(这里使用的是局部拦截器),效果如下:
oooo.jpg

1.3 响应映射拦截器(一)

我们已经知道CallHandler对象的handler()返回一个Observable流对象,此流包含了从路由处理程序返回的值,而rxjs/operators提供了非常丰富的运算符操作流,使用map()可以控制路由方法的返回值。下面将定义一个响应结果格式转换拦截器:

  1. // src/common/interceptors/transform.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. ExecutionContext,
  6. CallHandler,
  7. } from '@nestjs/common';
  8. import { Observable } from 'rxjs';
  9. import { map } from 'rxjs/operators';
  10. @Injectable()
  11. export class TransformInterceptor implements NestInterceptor {
  12. intercept(
  13. context: ExecutionContext,
  14. next: CallHandler<any>,
  15. ): Observable<any> | Promise<Observable<any>> {
  16. return next.handle().pipe(
  17. map((data) => {
  18. console.log("目标拦截方法,返回结果:",data); //目标拦截方法,返回结果:hello
  19. return { data };
  20. }),
  21. );
  22. }
  23. }
  1. import { Controller, Get, UseInterceptors } from '@nestjs/common';
  2. import { AppService } from './app.service';
  3. import { TransformInterceptor } from './common/interceptors/transform.interceptor';
  4. @Controller()
  5. export class AppController {
  6. constructor(private readonly appService: AppService) {}
  7. @Get('/hello')
  8. @UseInterceptors(TransformInterceptor)
  9. hello(): string {
  10. return 'hello';
  11. }
  12. }

访问localhost:3000/hello,效果如下图,预期结果应该是”hello”才对,但是实际返回一个具有data属性且值为”hello”的对象, 这是因为响应结果拦截器生效了,借助于rxjs/operators提供的map()运算符使得我们可以控制路由方法的返回值。

llllll.jpg

1.4 响应映射拦截器(二)

拦截器在创建用于整个应用程序的可重用解决方案时具有巨大的潜力。例如,当目标拦截方法的返回值是null时就转为字符串,并且我们可以把这个拦截器设置为全局拦截器,这样它会被每个注册的处理程序自动重用。

  1. // src/common/interceptors/excludeNull.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. ExecutionContext,
  6. CallHandler,
  7. } from '@nestjs/common';
  8. import { Observable } from 'rxjs';
  9. import { map } from 'rxjs/operators';
  10. /**
  11. * 排除null拦截器
  12. */
  13. @Injectable()
  14. export class ExcludeNullInterceptor implements NestInterceptor {
  15. intercept(
  16. context: ExecutionContext,
  17. next: CallHandler<any>,
  18. ): Observable<any> | Promise<Observable<any>> {
  19. return next.handle().pipe(map((data) => data || ''));
  20. }
  21. }

1.5 异常拦截器

catchError() 操作符来覆盖抛出的异常。

  1. // src/common/interceptors/exception.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. CallHandler,
  6. ExecutionContext,
  7. BadGatewayException,
  8. } from '@nestjs/common';
  9. import { Observable, throwError } from 'rxjs';
  10. import { catchError } from 'rxjs/operators';
  11. /* 当目标拦截函数中抛出异常时就会catchError()中的逻辑 */
  12. @Injectable()
  13. export class ExceptionInterceptor implements NestInterceptor {
  14. intercept(
  15. context: ExecutionContext,
  16. next: CallHandler<any>,
  17. ): Observable<any> | Promise<Observable<any>> {
  18. return next.handle().pipe(
  19. catchError((err) => {
  20. console.log('异常信息:', err);
  21. return throwError(new BadGatewayException());
  22. }),
  23. );
  24. }
  25. }
  1. import {
  2. Controller,
  3. Get,
  4. UseInterceptors,
  5. ForbiddenException,
  6. } from '@nestjs/common';
  7. import { AppService } from './app.service';
  8. import { ExceptionInterceptor } from './common/interceptors/exception.interceptor';
  9. @Controller()
  10. export class AppController {
  11. constructor(private readonly appService: AppService) {}
  12. @Get('/hello')
  13. @UseInterceptors(ExceptionInterceptor)
  14. hello(): string {
  15. //手动抛出禁止访问异常,http响应码是403
  16. throw new ForbiddenException('no auth...');
  17. return 'hello';
  18. }
  19. }

测试:
00000000.jpg

1.6 stream 重写

我们知道执行next.handle()会调用目标拦截函数,有时候我们并不需要调用目标拦截函数,例如使用缓存时,如果请求命中缓存就从缓存中获取数据返回,未命中缓存则正常执行目标拦截函数,命名缓存是不需要执行目标拦截函数的,我们可以使用of()运算符创建并返回一个新的流,因此目标拦截方法不会被调用的。

  1. // src/common/interceptors/cache.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. CallHandler,
  6. ExecutionContext,
  7. } from '@nestjs/common';
  8. import { Observable, of } from 'rxjs';
  9. @Injectable()
  10. export class CacheInterceptor implements NestInterceptor {
  11. intercept(
  12. context: ExecutionContext,
  13. next: CallHandler<any>,
  14. ): Observable<any> | Promise<Observable<any>> {
  15. const isCached = true;
  16. if (isCached) {
  17. //of()运算符会创建并返回一个新的流,当访问目标拦截器函数时会返回一个[]
  18. return of([]);
  19. }
  20. return next.handle();
  21. }
  22. }

1.7 超时拦截器

使用 RxJS 运算符操作流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。假设您要处理路由请求超时。如果您的端点在一段时间后未返回任何内容,则将以错误响应终止。

  1. // src/common/interceptors/timeout.interceptor.ts
  2. import {
  3. Injectable,
  4. NestInterceptor,
  5. CallHandler,
  6. ExecutionContext,
  7. RequestTimeoutException,
  8. } from '@nestjs/common';
  9. import { Observable, TimeoutError, throwError } from 'rxjs';
  10. import { catchError, timeout } from 'rxjs/operators';
  11. /**
  12. * 超时拦截器
  13. */
  14. @Injectable()
  15. export class TimeoutInterceptor implements NestInterceptor {
  16. intercept(
  17. context: ExecutionContext,
  18. next: CallHandler<any>,
  19. ): Observable<any> | Promise<Observable<any>> {
  20. //如果目标拦截器函数未在5s内正常返回则抛出请求超时异常
  21. return next.handle().pipe(
  22. timeout(5000),
  23. catchError((err) => {
  24. //可以在抛出异常之前添加自定义逻辑,例如释放资源等等
  25. if (err instanceof TimeoutError) {
  26. return throwError(new RequestTimeoutException());
  27. }
  28. return throwError(err);
  29. }),
  30. );
  31. }
  32. }