RxJS 在 Interceptor 中的应用:tap、map、catchError、timeout 让响应处理更优雅


RxJS 在 Interceptor 中的应用:tap、map、catchError、timeout 让响应处理更优雅

大家好,今天我们来聊聊 NestJS 拦截器(Interceptor)和 RxJS 这对黄金搭档。如果你看过一些 NestJS 的源码或者写过复杂的拦截器,一定见过 next.handle().pipe(...) 这种写法。为什么拦截器要跟 RxJS 扯上关系?因为拦截器拿到的不是一个普通的返回值,而是一个 Observable 流。这意味着你可以用 RxJS 提供的各种操作符对这个流进行“加工”,从而实现对响应数据的统一处理、异常捕获、超时控制等。今天我们就挑几个最常用的操作符——tapmapcatchErrortimeout,结合实例看看它们能玩出什么花样。


为什么拦截器要用 RxJS?

首先要明白,拦截器的 intercept 方法返回的是一个 Observable。当你调用 next.handle() 时,它返回的就是控制器方法执行后的结果流。这个流可能是同步的(比如返回一个普通对象),也可能是异步的(比如返回 Promise 或 Observable)。RxJS 的强大之处就在于,你可以用一堆操作符对这个流进行链式处理,而不需要关心它是同步还是异步。

所以,拦截器天然就适合做响应数据的“加工厂”。而 RxJS 就是工厂里的流水线工具。


四大常用操作符详解

1. 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();
    return next.handle().pipe(
      tap(() => console.log(`请求处理耗时:${Date.now() - now}ms`)),
    );
  }
}

这里我们用 tap 在流完成(或发出值)时执行一个回调,打印耗时。因为 tap 不会修改数据,所以对下游完全透明。

2. map:对响应数据“改头换面”

map 操作符允许你对流中的每一个值进行变换。在拦截器里,最常见的用途就是把控制器返回的数据统一包装成固定的格式,比如 { code: 0, data: ..., message: 'success' }

假设我们希望所有的成功响应都包一层:

import { map } from 'rxjs/operators';

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

这样,不管控制器返回什么,最终客户端收到的都是统一结构的 JSON。如果你的团队有统一的 API 响应规范,这种拦截器就非常实用。

3. catchError:优雅地处理异常

catchError 用来捕获流中的错误,然后返回一个新的 Observable(比如一个默认值或一个错误响应)。它可以在异常到达 ExceptionFilter 之前就被拦截器处理掉。

举个例子,你想把某些错误转换成友好的错误消息,或者记录错误日志后再抛出:

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      catchError(err => {
        console.error('发生错误:', err.message);
        // 可以选择抛出一个自定义异常
        return throwError(() => new BadRequestException('请求处理失败,请稍后重试'));
      }),
    );
  }
}

注意,catchError 必须返回一个 Observable,通常用 throwError 重新抛出异常,或者返回一个默认值让流继续。

4. timeout:给接口设置超时

有时候我们希望某个接口不能耗时太长,超过一定时间就自动返回超时错误。timeout 操作符正是干这个的。

import { timeout, catchError } from 'rxjs/operators';
import { throwError, TimeoutError } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000), // 5秒超时
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException('请求超时'));
        }
        return throwError(() => err);
      }),
    );
  }
}

这里我们设置了 5 秒超时,如果控制器方法执行超过 5 秒还没返回,就会抛出 TimeoutError,然后我们在 catchError 里将其转换成 NestJS 内置的 RequestTimeoutException(最终会被 ExceptionFilter 处理成 408 响应)。


组合起来更强大

这些操作符可以链式组合,实现更复杂的逻辑。比如我们既要统一包装响应,又要处理超时,还要记录日志:

return next.handle().pipe(
  timeout(5000),
  tap(() => console.log('请求完成')),
  map(data => ({ code: 0, data, message: 'success' })),
  catchError(err => {
    if (err instanceof TimeoutError) {
      return throwError(() => new RequestTimeoutException('超时'));
    }
    return throwError(() => err);
  }),
);

注意顺序:timeout 在最前面,因为它会监测整个流的执行时间;tapmap 之前,可以记录原始数据(但这里在 map 之前,记录的是原始数据,如果你想记录包装后的数据,可以把 tap 放在 map 之后);catchError 通常在最后,捕获前面所有操作符可能抛出的错误。


拦截器中的 RxJS 有什么限制?

虽然 RxJS 操作符很多,但实际在拦截器里常用的其实就那么几个。文件里也说了:“常用的 operator 也就这么几个”。因为拦截器的主要场景就是处理响应,而响应流通常只发出一项(然后完成),所以像 mergeMapswitchMap 这类用于扁平化多个异步操作的运算符反而用得少。但如果你需要,也可以结合使用,比如在拦截器里调用另一个异步服务后再返回最终数据。


总结

RxJS 让 NestJS 的拦截器拥有了数据流处理的能力,而 tapmapcatchErrortimeout 这四个操作符几乎覆盖了拦截器 90% 的需求:

  • tap:做副作用,不修改数据。
  • map:修改响应数据,比如统一包装。
  • catchError:捕获并处理错误。
  • timeout:设置超时控制。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - RxJS 在 Interceptor 中的应用:tap、map、catchError、timeout 让响应处理更优雅


Carpe Diem and Do what I like