深入浅出 RxJS 与 Nest 拦截器:异步逻辑的优雅处理

RxJS 初识

RxJS 是一个组织异步逻辑的库,它有很多 operator,可以极大的简化异步逻辑的编写。

它是由数据源(observable)产生数据,经过一系列 operator 的处理,最后传给接收者。

比如这样:

调用 of 操作符创建一个 Observable,它发送了三个值 1、2、3。

使用 pipe 方法,将 map 操作符添加到 Observable 上,对每个输入值进行平方运算,得到一个新的 Observable,它发送了 1、4、9。

再次使用 pipe 方法,将 filter 操作符添加到 Observable 上,过滤掉所有偶数值,得到一个新的 Observable,它发送了 1、9。

最后,调用 subscribe 方法订阅这个 Observable,当它发送新值时,调用提供的回调函数输出结果。在这个例子中,回调函数输出了每个接收到的值。

继续看这段代码:

调用 of 操作符创建一个 Observable,它发送了三个值 1、2、3。

使用 pipe 方法,将 scan 操作符添加到 Observable 上,对每个输入值进行累加操作,得到一个新的 Observable,它在每次接收到值时都会发出累加结果。在这个例子中,第一个值是 1,第二个值是 1 + 2 = 3,第三个值是 1 + 2 + 3 = 6。

再次使用 pipe 方法,将 map 操作符添加到 Observable 上,对每个累加结果进行平均数计算,得到一个新的 Observable,它在每次接收到值时都会发出平均数结果。在这个例子中,第一个值是 1,第二个值是 (1 + 2) / 2 = 1.5,第三个值是 (1 + 2 + 3) / 3 = 2。

最后,调用 subscribe 方法订阅这个 Observable,当它发送新值时,调用提供的回调函数输出结果。在这个例子中,回调函数输出了每个接收到的平均数结果。

再来看节流、防抖:

可以在官网文档看到所有的 operator

如果异步逻辑复杂度高, RxJS 收益还是很高的。

Nest 的 interceptor 集成了 RxJS,可以用它来处理响应。

拦截器初识

创建一个测试项目:

bash 复制代码
nest new interceptor-test -p npm

进入目录执行:

bash 复制代码
nest g interceptor logging --flat --no-spec

记录下请求时间:

使用拦截器的方式:

方法级别:

访问页面,控制台打印:

全局:

typescript 复制代码
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';

@Module({
  // ...
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

控制器级别:

typescript 复制代码
import { Controller, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('cats')
@UseInterceptors(LoggingInterceptor)
export class CatsController {
  // ...
}

我们再来看看适合在 Nest 的 interceptor 里用的 operator:

Nest 中使用 RxJS

map

map 操作符允许对从请求处理程序返回的数据进行转换。例如,可以使用它将数据包装在一个标准的响应对象中,该对象包含状态码、消息和数据。

生成一个 interceptor:

bash 复制代码
nest g interceptor map-test --flat --no-spec

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

javascript 复制代码
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,
        };
      }),
    );
  }
}

controll 下启用:

请求接口:

tap

tap 操作符允许在不修改数据流的情况下执行副作用操作,比如记录日志或更新缓存。

这对于在请求处理过程中添加额外的日志记录或调试信息非常有用。

再生成个 interceptor

bash 复制代码
nest g interceptor tap-test --flat --no-spec

使用 tap operator 来添加一些日志、缓存等逻辑:

javascript 复制代码
import { AppService } from './app.service';
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  Logger,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class TapTestInterceptor implements NestInterceptor {
  constructor(private appService: AppService) {}

  private readonly logger = new Logger(TapTestInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      tap((data) => {
        // 这里假装下更新缓存的操作
        this.appService.getHello();

        this.logger.log(`log something`, data);
      }),
    );
  }
}

在 controller 返回响应的时候记录一些东西。

catchError

catchError 操作符允许处理在请求处理程序中抛出的异常。

可以先在拦截器中捕获这些异常,并执行一些额外的逻辑,比如记录错误日志或返回自定义的错误响应。

生成 interceptor:

bash 复制代码
nest g interceptor catch-error-test --flat --no-spec

使用 catchError 处理抛出的异常:

typescript 复制代码
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 操作符来捕获异常
      catchError((err) => {
        this.logger.error(err.message, err.stack);

         // 重新抛出错误,让它可以被后续的错误处理器捕获
        return throwError(() => err);
      }),
    );
  }
}

在 controller 使用下:

这个 500 错误,是内置的 exception filter 处理的:

nest 控制台会打印两次错误,报错信息都是 xxx。

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

timeout

timeout 操作符允许为请求处理程序设置一个超时时间。

如果在指定的时间内没有收到响应,它将抛出一个 TimeoutError 异常。

可以结合 catchError 操作符来处理这个异常,并返回一个适当的超时响应。

创建 interceptor:

typescript 复制代码
nest g interceptor timeout --flat --no-spec

添加如下逻辑:

typescript 复制代码
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Observable, TimeoutError, throwError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 设置超时时间为3秒
    const TIMEOUT = 3000;

    return next.handle().pipe(
      timeout(TIMEOUT), // 在3秒后如果还没有响应,则抛出TimeoutError
      catchError((error) => {
        if (error instanceof TimeoutError) {
          // 如果捕获到超时错误,则抛出HttpException
          return throwError(
            () => new HttpException('请求超时', HttpStatus.REQUEST_TIMEOUT),
          );
        }
        // 如果是其他类型的错误,则继续抛出
        return throwError(() => error);
      }),
    );
  }
}

这样 timeout 操作符会在 3s 没收到消息的时候抛一个错误。

在 controller 使用下:

浏览器访问,3s 后返回 408 响应:

Interceptor 和 Middleware 的区别

主要区别是 Interceptor 可以拿到调用的 controller 和 handler,而 middleware 不行。

typescript 复制代码
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    
    const controller = context.getClass(); // 获取当前调用的控制器
    const handler = context.getHandler(); // 获取当前调用的处理器
    
    // 执行下一个中间件或处理器
    return next.handle().pipe(
      tap(() => console.log('After...'))
    );
  }
}
相关推荐
如若123几秒前
对文件内的文件名生成目录,方便查阅
java·前端·python
初晴~30 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813635 分钟前
InnoDB 的页分裂和页合并
数据库·后端
滚雪球~1 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport1 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg1 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala