nestjs学习13:interceptor与rxjs

35 阅读4分钟

关于对rxjs的描述,请参考这篇文章对rxjs的理解和基本使用

interceptor都是和rxjs配合使用的,先看看在interceptor中有那些常用的操作符。

map

生成一个 interceptor:

nest g interceptor map-test --no-spec --flat

使用 map operator 来对 controller 返回的数据做一些修改:

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

@Injectable()
export class MapTestInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map(data => {
      return {
        code: 200,
        message: 'success',
        data
      }
    }))
  }
}

在 controller 里引入下:

image.png

image.png

现在返回的数据就变成了这样。

map 算是在 nest interceptor 里必用的 rxjs operator 了。

可以看到interceptor使用的第一个场景:转换请求 / 响应数据(最常用)

上面其实展示了如何转换响应数据,下面再看看如何转换请求数据:

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

@Injectable()
export class RequestTransformInterceptor implements NestInterceptor {
  // 核心方法:拦截请求,处理入参后再执行控制器逻辑
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 1. 获取 HTTP 上下文(拿到请求对象)
    const ctx = context.switchToHttp();
    const request = ctx.getRequest();

    // 2. 统一处理入参:覆盖原始请求的 query/params/body
    // 处理 URL 参数(如 /users/:id → id 从字符串转数字)
    if (request.params) {
      request.params = this.transformParams(request.params);
    }

    // 3. 执行控制器方法(此时入参已被转换)
    return next.handle();
  }

一般拦截器Interceptor是全局批量转换请求参数,单个参数的精准转换 + 验证,还是要使用管道 pipe。

tap

tap不会修改响应数据,而是执行一些额外逻辑,比如记录日志、更新缓存等

下面看一下记录日志的例子:

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> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();
    const { method, url } = request; // 获取请求方法和路径

    // 执行控制器方法,响应返回后触发 tap 回调
    return next.handle().pipe(
      tap(() => {
        console.log(`[${method}] ${url} 耗时: ${Date.now() - now}ms`);
        // 输出示例:[GET] /api/users 耗时: 15ms
      })
    );
  }
}

对于高频访问、数据变更少的接口(比如首页配置、商品分类),可以用拦截器 + tap 来缓存响应结果,指定时间内重复请求直接返回缓存,提升性能。

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

// 缓存容器
const cache = new Map<string, { data: any; expire: number }>();
const CACHE_TTL = 60 * 1000; // 缓存1分钟

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `${request.method}:${request.url}`; // 缓存键:方法+路径

    // 检查缓存是否存在且未过期
    const cached = cache.get(cacheKey);
    if (cached && Date.now() < cached.expire) {
      return of(cached.data); // 直接返回缓存数据,不执行控制器方法
    }

    // 执行控制器方法,缓存结果
    return next.handle().pipe(
      tap((data) => {
        cache.set(cacheKey, {
          data,
          expire: Date.now() + CACHE_TTL,
        });
      })
    );
  }
}

可以看到,在 tap 中把请求结果缓存起来,下次再用时直接返回缓存。

catchError

controller 里很可能会抛出错误,这些错误会被 exception filter 处理,返回不同的响应,但在那之前,我们可以在 interceptor 里先处理下。

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

@Injectable()
export class CatchErrorTestInterceptor implements NestInterceptor {
  private readonly logger = new Logger(CatchErrorTestInterceptor.name)

  intercept (context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(catchError(err => {
      this.logger.error(err.message, err.stack)
      return throwError(() => err)
    }))
  }
}

这里我们就是日志记录了一下,当然你也可以改成另一种错误,重新 throwError。

image.png

打印了两次错误:

image.png

一次是我们在 interceptor 里打印的,一次是 exception filter 打印的。

timeout

接口如果长时间没返回,要给用户一个接口超时的响应,这时候就可以用 timeout operator。

import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
import { catchError, Observable, throwError, timeout, TimeoutError } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(3000),
      catchError(err => {
        if(err instanceof TimeoutError) {
          console.log(err);
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      })
    )
  }
}

imeout 操作符会在 3s 没收到消息的时候抛一个 TimeoutError。

然后用 catchError 操作符处理下,如果是 TimeoutError,就返回 RequestTimeoutException,这个有内置的 exception filter 会处理成对应的响应格式。

其余错误就直接 throwError 抛出去。

image.png

最后,再来看下全局的 interceptor:

image.png

因为这种是手动 new 的,没法注入依赖。

但很多情况下我们是需要全局 interceptor 的,而且还用到一些 provider,怎么办呢?

nest 提供了一个 token,用这个 token 在 AppModule 里声明的 interceptor,Nest 会把它作为全局 interceptor:

image.png

在这个 interceptor 里我们注入了 appService:

image.png

总结

rxjs 是一个处理异步逻辑的库,它的特点就是 operator 多,你可以通过组合 operator 来完成逻辑,不需要自己写。

nest 的 interceptor 就用了 rxjs 来处理响应,但常用的 operator 也就这么几个:

  • tap: 不修改响应数据,执行一些额外逻辑,比如记录日志、更新缓存等
  • map:对响应数据做修改,一般都是改成 {code, data, message} 的格式
  • catchError:在 exception filter 之前处理抛出的异常,可以记录或者抛出别的异常
  • timeout:处理响应超时的情况,抛出一个 TimeoutError,配合 catchErrror 可以返回超时的响应

总之,rxjs 的 operator 多,但是适合在 nest interceptor 里用的也不多。

此外,interceptor 也是可以注入依赖的,你可以通过注入模块内的各种 provider。

全局 interceptor 可以通过 APP_INTERCEPTOR 的 token 声明,这种能注入依赖,比 app.useGlobalInterceptors 更好。

interceptor 是 nest 必用功能,还是要好好掌握的。