nestjs学习 - 中间件(Middleware)

请求生命周期

了解中间件之前,需要先对整个 请求生命周期 有一定的认识:

请求进入 → 中间件 → 守卫 → 拦截器 → 管道 → 控制器 → 服务 → 拦截器 → 异常过滤器 → 服务器响应

  1. 请求进入

  2. 中间件

    • 2.1. 全局绑定的中间件
    • 2.2. 模块绑定的中间件
  3. 守卫

    • 3.1 全局守卫
    • 3.2 控制器守卫
    • 3.3 路由守卫
  4. 拦截器(控制器前)

    • 4.1 全局拦截器
    • 4.2 控制器拦截器
    • 4.3 路由拦截器
  5. 管道

    • 5.1 全局管道
    • 5.2 控制器管道
    • 5.3 路由管道
    • 5.4 路由参数管道
  6. 控制器(方法处理程序)

  7. 服务(如果存在)

  8. 拦截器(请求后)

    • 8.1 路由拦截器
    • 8.2 控制器拦截器
    • 8.3 全局拦截器
  9. 异常过滤器

    • 9.1 路由过滤器
    • 9.2 控制器过滤器
    • 9.3 全局过滤器
  10. 服务器响应

中间件

一、它是什么

官网描述:

Middleware是在路由处理程序之前 调用的函数。中间件函数可以访问请求响应对象,以及应用程序的请求-响应周期中的next()中间件函数。通常,next 中间件函数由一个名为next的变量表示。

Nest中间件默认情况下与express中间件等效。以下来自官方express文档的描述介绍了中间件的功能:

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

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

白话文:用来在请求到达路由处理器之前或之后执行特定的逻辑。

二、 使用

参考官网:nestjs.inode.club/middleware

  • 你的第一个中间件

    logger.middleware.ts

    typescript 复制代码
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response, NextFunction } from 'express';
    ​
    @Injectable()
    export class LoggerMiddleware implements NestMiddleware {
      use(req: Request, res: Response, next: NextFunction) {
        console.log('Request...');
        next();
      }
    }
  • app.module.ts 中使用

    typescript 复制代码
    import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
    import { LoggerMiddleware } from './common/middleware/logger.middleware';
    import { CatsModule } from './cats/cats.module';
    import { CatsController } from './cats/cats.controller'
    ​
    @Module({
      imports: [CatsModule],
    })
    export class AppModule implements NestModule {
      configure(consumer: MiddlewareConsumer) {
        consumer
          .apply(LoggerMiddleware)
          .forRoutes('cats');
          // .forRoutes({ path: 'cats', method: RequestMethod.GET });
          // .forRoutes({ path: 'cats/*', method: RequestMethod.ALL });
          // .forRoutes(CatsController);
      }
    }

    注意以上代码,forRoutes 有多中写法,还有 路由排除:exclude

    • 支持 - 路由通配符写法:路由路径 'ab*cd' 将匹配 abcdab_cdabecd 等等。字符 ?+*() 可以在路由路径中使用,它们是正则表达式的子集。连字符(-)和点号(.)在基于字符串的路径中被解释为字面量。

    • exclude 路由排除:

      php 复制代码
      consumer
        .apply(LoggerMiddleware)
        .exclude(
          { path: 'cats', method: RequestMethod.GET },
          { path: 'cats', method: RequestMethod.POST },
          'cats/(.*)',
        )
        .forRoutes(CatsController);
  • 函数式中间件

    没有成员,没有额外的方法,也没有依赖关系 时,可以使用它;

    vbscript 复制代码
    import { Request, Response, NextFunction } from 'express';
    ​
    export function logger(req: Request, res: Response, next: NextFunction) {
      console.log(`Request...`);
      next();
    };
  • 多个中间件

    正如上面所述,为了绑定多个按顺序执行的中间件,只需在 apply() 方法内提供一个逗号分隔的列表:

    scss 复制代码
    consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
  • 全局中间件

    如果我们想要一次性将中间件绑定到每个已注册的路由上,我们可以使用 INestApplication 实例提供的 use() 方法:

    ini 复制代码
    // main.ts 中
    const app = await NestFactory.create(AppModule);
    app.use(logger);
    await app.listen(3000);

三、使用场景

主要作用:

  • 请求预处理:在请求到达控制器之前对请求进行检查、修改或增强。
  • 响应后处理:在响应发送给客户端之前对响应进行修改或记录。
  • 流程控制:决定是否继续处理请求或直接返回响应。

使用场景:

Expressfastify处理中间件的方式不同,并提供不同的方法签名,详情请阅读这里

1. 日志记录
typescript 复制代码
// logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
​
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    const start = Date.now();
    
    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${duration}ms`);
    });
    
    next();
  }
}
2. 身份验证和授权
typescript 复制代码
// auth.middleware.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
​
@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    const token = req.headers['authorization'];
    
    if (!token) {
      throw new UnauthorizedException('Token is required');
    }
    
    // 验证 token 逻辑
    const user = this.validateToken(token);
    if (!user) {
      throw new UnauthorizedException('Invalid token');
    }
    
    // 将用户信息附加到请求对象
    (req as any).user = user;
    next();
  }
  
  private validateToken(token: string) {
    // JWT 验证逻辑
    return { id: 1, username: 'john' };
  }
}
3. 请求数据验证和转换
typescript 复制代码
// validation.middleware.ts
import { Injectable, NestMiddleware, BadRequestException } from '@nestjs/common';
​
@Injectable()
export class ValidationMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    // 验证 Content-Type
    if (req.headers['content-type'] !== 'application/json') {
      throw new BadRequestException('Only JSON content is allowed');
    }
    
    // 验证请求体大小
    const contentLength = parseInt(req.headers['content-length']);
    if (contentLength > 1024 * 1024) { // 1MB
      throw new BadRequestException('Request body too large');
    }
    
    next();
  }
}
4. 限流和频率控制
typescript 复制代码
// rate-limiting.middleware.ts
import { Injectable, NestMiddleware, TooManyRequestsException } from '@nestjs/common';
​
@Injectable()
export class RateLimitingMiddleware implements NestMiddleware {
  private requests = new Map<string, number[]>();
  
  use(req: Request, res: Response, next: Function) {
    const ip = req.ip;
    const now = Date.now();
    const windowMs = 15 * 60 * 1000; // 15分钟
    const maxRequests = 100; // 最大100个请求
    
    const userRequests = this.requests.get(ip) || [];
    
    // 清理过期的请求记录
    const recentRequests = userRequests.filter(time => now - time < windowMs);
    
    if (recentRequests.length >= maxRequests) {
      throw new TooManyRequestsException('Too many requests');
    }
    
    recentRequests.push(now);
    this.requests.set(ip, recentRequests);
    
    // 设置限流头信息
    res.setHeader('X-RateLimit-Limit', maxRequests);
    res.setHeader('X-RateLimit-Remaining', maxRequests - recentRequests.length);
    
    next();
  }
}
5. CORS 处理
typescript 复制代码
// cors.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
​
@Injectable()
export class CorsMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    res.setHeader('Access-Control-Allow-Origin', 'https://example.com');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    
    // 处理预检请求
    if (req.method === 'OPTIONS') {
      res.status(204).send();
      return;
    }
    
    next();
  }
}
6. 数据压缩和缓存
typescript 复制代码
// compression.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import * as compression from 'compression';
​
@Injectable()
export class CompressionMiddleware implements NestMiddleware {
  private compression = compression();
  
  use(req: Request, res: Response, next: Function) {
    // 对响应进行压缩
    this.compression(req, res, next);
  }
}
7. API 版本控制
typescript 复制代码
// versioning.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
​
@Injectable()
export class VersioningMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    const version = req.headers['api-version'] || 'v1';
    
    // 根据版本号设置不同的行为
    (req as any).apiVersion = version;
    
    // 可以在这里根据版本重写 URL 或设置不同的逻辑
    if (version === 'v2') {
      // v2 特定的预处理
    }
    
    next();
  }
}
8. 多租户支持
typescript 复制代码
// tenant.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
​
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    const tenantId = req.headers['x-tenant-id'] || req.hostname;
    
    if (!tenantId) {
      throw new Error('Tenant identification required');
    }
    
    // 设置租户上下文
    (req as any).tenant = {
      id: tenantId,
      // 可以根据租户ID加载特定配置
      config: this.loadTenantConfig(tenantId)
    };
    
    next();
  }
  
  private loadTenantConfig(tenantId: string) {
    // 加载租户特定配置
    return { database: `tenant_${tenantId}`, theme: 'default' };
  }
}

四、总结

中间件的优势

  1. 代码复用:相同的逻辑可以在多个路由间共享
  2. 关注点分离:将横切关注点与业务逻辑分离
  3. 可维护性:中间件逻辑集中管理,易于维护
  4. 灵活性:可以轻松添加、移除或修改中间件
  5. 性能优化:可以在早期拒绝无效请求,减少不必要的处理

使用建议

  • 中间件应该保持轻量级,避免复杂的业务逻辑
  • 对于复杂的业务验证,考虑使用守卫(Guards)或管道(Pipes)
  • 使用异常过滤器来处理中间件中抛出的异常
  • 考虑中间件的执行顺序对性能的影响
相关推荐
蜡台12 小时前
Uniapp H5Builderx 预览Html 显示404问题解决
前端·uni-app
We་ct12 小时前
LeetCode 190. 颠倒二进制位:两种解法详解
前端·算法·leetcode·typescript
踩着两条虫13 小时前
AI驱动的Vue3应用开发平台深入探究(二十五):API与参考之Renderer API 参考
前端·javascript·vue.js·人工智能·低代码·前端框架·ai编程
信创DevOps先锋13 小时前
本土化突围:Gitee如何重新定义企业级项目管理工具价值
前端·gitee·jquery
圣光SG13 小时前
Java类与对象及面向对象基础核心详细笔记
java·前端·数据库
Jinuss13 小时前
源码分析之React中的useImperativeHandle
开发语言·前端·javascript
ZC跨境爬虫13 小时前
CSS核心知识点与定位实战全解析(结合Playwright爬虫案例)
前端·css·爬虫
Jinuss13 小时前
源码分析之React中的forwardRef解读
前端·javascript·react.js
mengsi5513 小时前
Antigravity IDE 在浏览器上 verify 成功但本地 IDE 没反应 “开启Tun依然无济于事” —— 解决方案
前端·ide·chrome·antigravity