【Nest.js 10】拦截器&统一返回数据格式

982 阅读3分钟

项目地址:github.com/cwjbjy/nest…

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

从控制台的打印结果可以看出,拦截器是符合洋葱模型的,这里放一张洋葱模型图片

clipboard.png

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文章推荐:

  1. 前端想了解后端?那得先学会 TypeScript 装饰器!

  2. Nest.js 从零到壹详细系列(一):项目创建&文件分析

  3. Nest.js 从零到壹详细系列(二):控制器

  4. Nest.js 从零到壹详细系列(三):模块Module

  5. Nest.js 从零到壹详细系列(四):异常过滤器&错误日志收集

后续会继续更新Nest系列文章,感兴趣的可先关注我。