前端也想写后端(3)中间件Middleware、管道Pipes、拦截器Interceptors

上节我们介绍了在Nest.js中如何使用TypeORM进行数据库的操作,也写了几个简单的接口,但是接口的返回值没有任何的格式,但是在实际的开发中,我们需要将接口返回值进行格式化,比如返回值中需要包含状态码、消息、数据等,这就需要我们使用到Nest.js中的拦截器了,但是在介绍拦截器之前,我们先来了解一下Nest.js中的中间件和管道。

中间件

中间件是在路由处理器之前 调用的函数,可以访问请求响应 的对象,以及应用程序的请求-响应周期中的 next() 中间件函数。默认情况下,Nest的中间件等同于Express的中间件。

中间件函数可以执行以下任务:

  • 执行任何代码
  • 对请求和响应对象进行更改
  • 结束请求-响应周期
  • 调用堆栈中的下一个中间件函数
  • 如果当前中间件函数没有结束请求-响应周期,则必须调用 next() 将控制权传递给下一个中间件函数,否则,请求将被挂起。

一般情况下我们会在中间件中处理一些公共的逻辑,比如日志记录、身份验证、请求拦截/限制、响应拦截、跨域处理等。

那怎么实现中间件呢?在Nest.js中,我们可以通过实现 NestMiddleware 接口来创建中间件,然后使用 @Injectable() 装饰器将其标记为提供者。

ts 复制代码
import { NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";

export class TestMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log("Test Middleware", req.baseUrl, req.method);
    next();
  }
}

中间件可以在模块级别或全局级别注册。先说某个模块上注册,我们将这个中间件注册到 app.module 模块中

ts 复制代码
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { TestMiddleware } from "./test.middleware";

@Module()
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(TestMiddleware).forRoutes('test');
  }
}

这样我们就创建了一个中间件,并将其注册到了 test 路由上,当访问 test 路由时,中间件就会被触发。

我们使用 nest g resource test 生成一个模块,并使用RERSTful API生成一些CURD示例,这时候我们访问 http://localhost:3000/test 看一下

我们也可以访问上节的一些接口,比如 http://localhost:3000/user,看一下中间件是否生效

这个forRoutes方法还可以接受一个数组,用于指定多个路由,比如 forRoutes('test', 'user'),这样中间件就会在 testuser 路由上生效,也支持使用通配符,比如 forRoutes('*')forRoutes('user/*'),也可以指定请求方法。

ts 复制代码
forRoutes({
  path: 'user/*',
  method: RequestMethod.GET, // 指定路由 user开头的所有GET请求
});

也可以使用exclude方法排除一些路由,比如 exclude('user'),这样中间件就不会在 user 路由上生效。

ts 复制代码
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(TestMiddleware).forRoutes('*').exclude('user');
  }
}

当有多个中间件时,可以使用 apply() 方法将它们组合在一起,比如 consumer.apply(TestMiddleware1, TestMiddleware2).forRoutes('test'),这样中间件就会按照顺序依次执行。

那如果我们想注册到全局的话,可以在 main.ts 中使用 app.use() 方法,比如 app.use(TestMiddleware),这样中间件就会在所有路由上生效。

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      stopAtFirstError: true,
    }),
  );
  app.use(TestMiddleware);
  await app.listen(process.env.PORT ?? 3000);
}

管道

管道是专门用于数据转换数据验证的类,它可以在数据从进入控制器之前或从控制器返回之后进行处理。管道可以用于以下场景:

  • 参数验证:可结合 class-validator 库使用,对请求参数进行验证,比如必填项、类型、长度等。
  • 数据转换:将请求参数转换为指定的类型,比如将字符串转换为数字、将日期字符串转换为日期对象等。
  • 数据清洗:对请求参数进行清洗,比如去除空格、去除特殊字符或者过滤敏感字段等。

Nest.js内置了几个常用的管道,比如 ValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeValidationPipe等,我们也可以自定义管道。

我们前面在 main.ts 中使用了 ValidationPipe,这个管道就是用于参数验证的,配合 class-validator 做数据验证,也可以使用 class-transformer 做数据转换。

那自定义管道怎么实现呢?在Nest.js中,我们可以通过实现 PipeTransform 接口来创建管道,并通过 transform 方法处理数据,然后使用 @Injectable() 装饰器将其标记为提供者。

ts 复制代码
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class TestPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    const val = parseInt(value, 16); // 按16进制解析字符串
    if (isNaN(val)) {
      throw new BadRequestException('参数类型错误');
    } else {
      return val;
    }
  }
}

transform 有两个参数,第一个参数 value 是当前处理的方法参数(在路由处理方法接收到它之前),第二个参数 metadata 是当前处理的方法参数的元数据,元数据中包含了请求参数的类型、请求参数的名称等信息,类型如下:

ts 复制代码
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

我们该怎么去用呢?我们打开 test.ontroller.ts 在里面引入我们的TestPipe,并且写一个新的方法,使用TestPipe对参数进行验证,代码如下:

ts 复制代码
@Get(':id')
findTargetOne(@Param('id', new TestPipe()) id: number) {
  return this.testService.findOne(id);
}

我们service中的findone方法没变,还是这个

ts 复制代码
findOne(id: number) {
  return `This action returns a #${id} test`;
}

这时候我们访问一下 http://localhost:3000/test/12.235 发送一个get请求,发现返回如下:

可以看到,我们传的参数是 12.235,但是我们的管道将参数转换为十六进制的整数类型,所以返回的结果是 This action returns a #18 test

这是局部使用管道,我们也可以全局使用管道,在 main.ts 中使用 app.useGlobalPipes() 方法,比如 app.useGlobalPipes(new ValidationPipe()),这样管道就会在所有路由上生效。

拦截器

拦截器是Nest.js中用于全局处理请求和响应的类,它可以在请求进入控制器之前或控制器返回响应之后进行处理。拦截器可以用于以下场景:

  • 认证和授权
  • 日志记录
  • 异常处理
  • 请求和响应的转换
  • 请求和响应的过滤

Nest.js内置了几个常用的拦截器,比如 LoggerInterceptorTimeoutInterceptorCacheInterceptorSerializeInterceptorCatchInterceptor等,我们也可以自定义拦截器。

那我们怎么实现拦截器呢?在Nest.js中,我们可以通过实现 NestInterceptor 接口来创建拦截器,并通过 intercept() 方法处理请求和响应,然后使用 @Injectable() 装饰器将其标记为提供者。

拦截器需要实现 NestInterceptor 接口的 intercept() 方法,该方法接收两个参数:

  • context: ExecutionContext:包含当前请求的上下文信息(如请求对象、控制器、方法等)。
  • next: CallHandler:用于获取控制器方法返回的 observable(表示方法的执行结果)。

我们可以在这个方法中响应进行处理,比如:

ts 复制代码
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// 定义统一响应结构
interface Response<T> {
  code: number;
  message: string;
  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) => {
        return {
          code: 200,
          message: 'success',
          data: data || null,
        };
      })
    )
  }
}

看这段代码似乎有点难懂,那我们先了解一下 rxjs 的概念。

rxjs 是一个用于处理异步数据流的库,它提供了一种可观察的对象(observable)来表示异步数据流,并且可以使用操作符(operator)对数据流进行处理。

在 Nest.js 中,控制器方法的返回值就是一个 observable,我们可以使用 pipe() 方法对它进行处理,比如:

ts 复制代码
@Get()
findAll(): Observable<any> {
  return this.testService.findAll();
}

我们可以在 findAll() 方法中使用 pipe() 方法对返回的 observable 进行处理,比如:

ts 复制代码
@Get()
findAll(): Observable<any> {
  return this.testService.findAll().pipe(map((data) => {
    return {
      code: 200,
      message: 'success',
      data: data || null,
    };
  }));
}

那 map 是做什么的,除了 map 还有哪些方法呢?我们这里列举一些常用的方法:

  • map():接收 Observable 流中的每一个数据,对其进行转换处理,并返回一个新的 Observable 流(包含转换后的数据)。
  • tap(): 执行副作用操作(如日志打印、计时、缓存存储),但不修改流中的数据,也不影响流的传递,可理解为"不修改数据,只做额外操作"。
  • filter():过滤 observable 的数据,只保留满足条件的值,比如做数据筛选(如只对 GET 请求的返回结果做缓存,POST 请求跳过)。
  • take():只取 Observable 流中的前 N 个数据,之后自动完成流(忽略后续数据),限制返回数据量(如缓存拦截中,只保留最新的 1 条缓存数据)。
  • retry: 当 Observable 流出错时,自动重试指定次数(若重试成功则继续,失败则抛出错误),临时故障恢复(如数据库连接偶发失败时,重试 2 次后再返回错误)。
  • catchError():捕获 observable 的错误,并返回一个新的 observable,比如自定义异常处理(如将业务错误转换为标准错误响应,替代默认的 500 页面)。
  • finalize():无论 Observable 流是 "成功完成" 还是 "出错终止",都会执行的逻辑(类似 finally),可以做资源释放(如请求结束后关闭数据库连接、清除临时变量)。

回头我们再理解上面的,在 intercept() 方法中,我们使用 next.handle() 获取控制器方法返回的 observable,然后使用 map() 方法将 observable 转换为一个新的 observable,新的 observable 的数据格式为 { code: 200, message: 'success', data: data || null }

我们将这个拦截器添加到全局,在 main.ts 中使用 app.useGlobalInterceptors() 方法,比如 app.useGlobalInterceptors(new TransformInterceptor()),这样拦截器就会在所有路由上生效。

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动移除未在 DTO 中定义的字段
      forbidNonWhitelisted: true, // 对未定义的字段抛出错误
      transform: true, // 自动将请求数据转换为 DTO 实例
      stopAtFirstError: true, // 在第一个验证错误时停止验证
    }),
  );
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

这时候我们访问上面我们写过的接口 http://localhost:3000/test/12.235,看一下返回结果

当然我们也是可以局部使用拦截器的,在控制器或者模块中使用 @UseInterceptors() 装饰器,比如在我们的test.controller中使用

ts 复制代码
@Get()
findAll() {
  return this.testService.findAll();
}

@Get(':id')
@UseInterceptors(TransformInterceptor)
findOne(@Param('id', new TestPipe()) id: number) {
  return this.testService.findOne(id);
}

我们分别访问 http://localhost:3000/testhttp://localhost:3000/test/12.235,看一下返回结果发现只有第二个接口返回的是我们重新定义的格式,第一个接口返回的还是原来的格式。

有时候我们想知道一个接口执行的时长,以监测某个接口是否存在性能问题,这时候我们可以使用拦截器来实现 monitor.interceptor.ts

ts 复制代码
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable, tap } from "rxjs";

@Injectable()
export class MonitorInterceptor implements NestInterceptor {
  private logger = new Logger('monitor')
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    const req = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        const time = Date.now() - start;
        this.logger.log(`Request ${req.method} ${req.url} took ${time}ms`);
      })
    )
  }
}

然后我们在全局引入这个拦截器

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/transform.interceptor';
import { MonitorInterceptor } from './common/monitor.interceptor'

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动移除未在 DTO 中定义的字段
      forbidNonWhitelisted: true, // 对未定义的字段抛出错误
      transform: true, // 自动将请求数据转换为 DTO 实例
      stopAtFirstError: true, // 在第一个验证错误时停止验证
    }),
  );
  app.useGlobalInterceptors(new TransformInterceptor());
  app.useGlobalInterceptors(new MonitorInterceptor());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

这时候我们随便请求我们的一个接口,比如还是这个 http://localhost:3000/test/12.235,看一下控制台输出

可以看到我们请求的接口执行了 1ms。

三者区别

特性 中间件(Middleware) 管道(Pipes) 拦截器(Interceptors)
核心功能 预处理请求 / 后处理响应(通用逻辑) 数据验证与转换(输入数据处理) 增强方法执行、拦截返回值 / 异常
执行时机 请求进入 → 路由匹配后,管道之前 中间件之后,控制器方法执行前 管道之后,控制器方法执行前后
处理对象 req/res 对象(请求上下文) 控制器方法的输入参数 控制器方法的执行过程与返回值
核心 API 实现 NestMiddleware 接口的 use() 实现 PipeTransform 接口的 transform() 实现 NestInterceptor 接口的 intercept()
典型用途 日志、认证、限流、跨域 参数校验、类型转换、数据清洗 响应格式化、缓存、异常捕获、性能监控

一个完整的请求生命周期中这三者的执行顺序是:

客户端请求 → 中间件(预处理) → 管道(验证/转换参数) → 控制器方法执行 → 拦截器(处理返回值) → 中间件(后处理,如记录耗时) → 响应客户端

本专栏源码地址

相关推荐
德育处主任13 分钟前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
mazhenxiao16 分钟前
qiankunjs 微前端框架笔记
前端
Q_Q51100828518 分钟前
python的软件工程与项目管理课程组学习系统
spring boot·python·django·flask·node.js·php·软件工程
无羡仙23 分钟前
事件流与事件委托:用冒泡机制优化前端性能
前端·javascript
秃头小傻蛋23 分钟前
Vue 项目中条件加载组件导致 CSS 样式丢失问题解决方案
前端·vue.js
CodeTransfer23 分钟前
今天给大家搬运的是利用发布-订阅模式对代码进行解耦
前端·javascript
阿邱吖24 分钟前
form.item接管受控组件
前端
韩劳模26 分钟前
基于vue-pdf实现PDF多页预览
前端
鹏多多27 分钟前
js中eval的用法风险与替代方案全面解析
前端·javascript
KGDragon27 分钟前
还在为 SVG 烦恼?我写了个 CLI 工具,一键打包,性能拉满!(已开源)
前端·svg