深入浅出 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...'))
    );
  }
}
相关推荐
2401_865854881 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
清灵xmf1 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨1 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL1 小时前
npm入门教程1:npm简介
前端·npm·node.js
AskHarries2 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622662 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化
程序员阿龙2 小时前
基于SpringBoot的医疗陪护系统设计与实现(源码+定制+开发)
java·spring boot·后端·医疗陪护管理平台·患者护理服务平台·医疗信息管理系统·患者陪护服务平台
程思扬2 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
小白白一枚1112 小时前
css实现div被图片撑开
前端·css
阿华的代码王国2 小时前
【Spring】——SpringBoot项目创建
java·spring boot·后端·启动类·target文件