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)
  • 使用异常过滤器来处理中间件中抛出的异常
  • 考虑中间件的执行顺序对性能的影响
相关推荐
像我这样帅的人丶你还1 小时前
2026前端技术从「夯」到「拉」
前端
烟雨落金城1 小时前
初识Electron,谈谈感悟
前端
jeff渣渣富1 小时前
Taro 小程序构建自动化:手写插件实现图片自动上传 OSS 并智能缓存
前端·webpack
恋猫de小郭2 小时前
谷歌 Genkit Dart 正式发布:现在可以使用 Dart 和 Flutter 构建全栈 AI 应用
android·前端·flutter
vim怎么退出3 小时前
谷歌性能优化知识点总结
前端
专业抄代码选手3 小时前
在react中,TSX是如何转变成JS的
前端·javascript
葡萄城技术团队3 小时前
【实践篇】从零到一:手把手教你搭建一套企业级 SpreadJS 协同设计器
前端
忆江南4 小时前
# iOS Block 深度解析
前端
米丘4 小时前
vue-router v5.x 路由模式关于 createWebHistory、 createWebHashHistory的实现
前端