Nest.js 中关于拦截器的使用

参考:

Nest.js:https://docs.nestjs.cn/

拦截器:https://docs.nestjs.cn/9/interceptors

基本使用

在 Nest.js 中,使用拦截器是非常简单的,以下是一个经典的处理请求超时的拦截器。

// timeout.interceptor.ts
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), // 超过 5000 毫秒没有响应就抛出 TimeoutError 
      catchError(err => { // catchError 实际上是可选的,如何不需要转换 Error 就不用写
        if (err instanceof TimeoutError) {
          return throwError(new RequestTimeoutException()); // 将 TimeoutError 转换为 RequestTimeoutException
        }
        return throwError(err);
      }),
    );
  };
};

通过拦截器,可以非常方便的在函数执行之前或之后添加额外的逻辑,以下是一个经典的打印用时日志的拦截器。

// logging.interceptor.ts
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`)),
      );
  }
}

接下来是绑定拦截器,如果只是在 class 级别调用,那么只需要这样写

// cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

在全局调用时,需要这样写

const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor()); // 此处设置拦截器

不过在通过app.useGlobalInterceptors方法全局调用的时候,无法向拦截器注入依赖,此时需要以下写法。

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor, // 通过这种方法绑定的拦截器可以随意添加依赖。
    },
  ],
})
export class AppModule {}

深入使用

拦截器中有一个功能叫【响应映射】,可以让我们非常方便的修改从路由处理程序返回的值。

不过正如文档中提到的一样,响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res()对象)

// transform.interceptor.ts
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 })));
  }
}

如果在路由中已经使用了 @Res() 对象,那么 next.handle().pipe(map(data => ({ data })))中的 data 就会变成 undefined,因此需要额外的逻辑处理,参考如下。

// exclude-null.interceptor.ts 排除 null 和 undefined 
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException } from '@nestjs/common'
import { Observable, throwError } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { Response } from 'express'

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next
            .handle()
            .pipe(
                map((data) => {
                    const response = context.switchToHttp().getResponse<Response>()
                    // 通过 response.headersSent 来判断是否已经响应
                    if (!response.headersSent && (typeof data === 'undefined' || data === null)) {
                        throw new HttpException('返回值为 undefined 或 null !', 400)
                    }
                    return data
                }),
            )
    }
}

异步逻辑

我们知道拦截器是可以在函数执行之前之后添加额外的逻辑 。

通过之前的案例,我们已经了解到在函数执行之后添加逻辑是通过 next.handle().pipe()来实现的,由于 Observable 天生自带异步处理逻辑,所以在执行之后添加异步逻辑也是非常轻松的。

那么问题来了,如果在函数执行之前要添加异步逻辑要如何处理?

答案是通过 Promise

通过观察 interface NestInterceptor 的类型定义可知,intercept 方法不仅可以返回一个 Observable,还可以返回一个 Promise<Observable>,这就意味着 intercept 方法可以通过返回 Promise 来异步执行逻辑。

例如:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable } from 'rxjs'

export async function sleep(time: number) {
    return new Promise((resolve) => setTimeout(resolve, time))
}

@Injectable()
export class CheckBanInterceptor implements NestInterceptor {

    async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
        await sleep(100) // 添加异步逻辑
        return next.handle()
    }
}

通过以上几种方法,我们就掌握了如何通过拦截器在函数执行之前和之后添加额外的逻辑,并且可以对返回值进行修改,同时还处理使用了 @Res() 对象的问题。