小满nestjs(第十三章 NestJS 中间件实战:从基础到高阶应用)
1. NestJS中间件基础入门第一次接触NestJS中间件时我把它想象成高速公路上的收费站。每辆汽车请求在到达目的地路由处理器之前都必须经过这些检查点。中间件最神奇的地方在于它能在请求到达控制器之前对请求进行各种预处理。让我们从最简单的日志中间件开始。这个中间件会记录每个请求的详细信息就像高速公路上的监控摄像头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([${new Date().toISOString()}] ${req.method} ${req.url}); next(); } }这个中间件做了三件事获取当前时间、记录请求方法和URL、然后调用next()让请求继续前进。如果不调用next()请求就会永远卡在这里就像收费站不放行车辆一样。在模块中注册这个中间件时我发现NestJS提供了非常灵活的绑定方式。你可以把它绑定到特定路由、特定控制器甚至特定HTTP方法// 绑定到特定路径 consumer.apply(LoggerMiddleware).forRoutes(user); // 绑定到GET方法 consumer.apply(LoggerMiddleware).forRoutes({ path: user, method: RequestMethod.GET }); // 绑定到整个控制器 consumer.apply(LoggerMiddleware).forRoutes(UserController);2. 构建电商后台中间件系统去年开发电商系统时我们遇到一个典型场景需要区分管理员和普通用户的API访问权限。这时候中间件就派上大用场了。我们设计了一个权限校验中间件它像安检门一样检查每个请求的权限标识。Injectable() export class AuthMiddleware implements NestMiddleware { constructor(private readonly userService: UserService) {} async use(req: Request, res: Response, next: NextFunction) { const token req.headers[authorization]; if (!token) { throw new UnauthorizedException(请先登录); } try { const user await this.userService.verifyToken(token); req.user user; // 将用户信息挂载到请求对象 next(); } catch (e) { throw new UnauthorizedException(令牌无效); } } }这个中间件有几个关键点值得注意通过依赖注入使用了UserService从请求头获取token验证失败时抛出标准异常验证成功后将用户信息挂载到请求对象在电商系统中我们还经常需要记录完整的请求日志。这时候可以组合多个中间件consumer .apply(LoggerMiddleware, AuthMiddleware) .forRoutes(OrderController);这种管道式的处理方式让代码既清晰又灵活。我实测下来这种设计在处理复杂业务逻辑时特别稳。3. 全局中间件与白名单机制有些中间件需要应用到所有路由比如我们之前提到的日志中间件或者跨域处理中间件。NestJS支持通过app.use()注册全局中间件但有个重要限制全局中间件不能使用依赖注入。我们曾用全局中间件实现了一个API白名单功能const whiteList [/api/login, /api/products]; export function ApiWhitelistMiddleware(req, res, next) { if (whiteList.includes(req.path) || req.user) { return next(); } res.status(403).json({ message: 访问受限 }); } // 在main.ts中注册 app.use(ApiWhitelistMiddleware);这里有个实用技巧白名单应该包含认证路由和公开API否则会陷入先有鸡还是先有蛋的困境 - 用户无法登录就无法获取token但没有token又不能访问登录接口。4. 第三方中间件集成实战NestJS完美兼容Express中间件生态这意味着海量的第三方中间件可以直接使用。以常用的CORS中间件为例npm install cors npm install types/cors --save-dev然后在main.ts中集成import * as cors from cors; async function bootstrap() { const app await NestFactory.create(AppModule); // 配置CORS app.use(cors({ origin: [https://our-shop.com, https://admin.our-shop.com], methods: GET,HEAD,PUT,PATCH,POST,DELETE, allowedHeaders: Content-Type,Authorization, })); await app.listen(3000); }在电商项目中我们还使用了helmet来提高安全性以及compression来压缩响应数据。这些中间件的集成方式都类似import * as helmet from helmet; import * as compression from compression; app.use(helmet()); app.use(compression());5. 中间件高级应用技巧经过多个项目实践我总结出几个中间件的高级用法。首先是错误处理中间件它能捕获整个应用抛出的异常Injectable() export class ErrorMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { try { next(); } catch (err) { // 统一错误处理逻辑 const status err.status || 500; res.status(status).json({ code: status, message: err.message || 服务器错误, timestamp: new Date().toISOString() }); } } }其次是性能监控中间件可以记录每个请求的处理时间Injectable() export class PerformanceMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const start Date.now(); res.on(finish, () { const duration Date.now() - start; console.log(${req.method} ${req.url} - ${duration}ms); }); next(); } }最后是请求转换中间件它能预处理请求数据。比如我们把所有输入的空字符串转为nullInjectable() export class TransformMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (req.body) { for (const key in req.body) { if (req.body[key] ) { req.body[key] null; } } } next(); } }6. 中间件调试与性能优化调试中间件时我踩过不少坑总结几个实用技巧。首先是中间件执行顺序问题NestJS会按照注册顺序执行中间件。有时候调换顺序就能解决奇怪的问题。其次是性能优化特别是在中间件中有异步操作时。比如数据库查询可以这样优化Injectable() export class CacheMiddleware implements NestMiddleware { constructor(private readonly cacheService: CacheService) {} async use(req: Request, res: Response, next: NextFunction) { const cacheKey route_${req.path}_${JSON.stringify(req.query)}; const cachedData await this.cacheService.get(cacheKey); if (cachedData) { return res.json(cachedData); } // 劫持原始send方法 const originalSend res.send; res.send (body) { this.cacheService.set(cacheKey, body, 60); // 缓存60秒 originalSend.call(res, body); }; next(); } }这个缓存中间件展示了如何巧妙劫持响应方法来实现高级功能。不过要注意内存泄漏问题在真实项目中需要更完善的实现。7. 测试中间件的正确方式测试中间件和测试控制器不太一样。我推荐使用nestjs/testing包提供的工具describe(AuthMiddleware, () { let middleware: AuthMiddleware; let userService: UserService; beforeEach(async () { const module await Test.createTestingModule({ providers: [ AuthMiddleware, { provide: UserService, useValue: { verifyToken: jest.fn() } } ] }).compile(); middleware module.getAuthMiddleware(AuthMiddleware); userService module.getUserService(UserService); }); it(应该拒绝无token的请求, () { const req { headers: {} } as Request; const res { status: jest.fn().mockReturnThis(), json: jest.fn() } as any; const next jest.fn(); middleware.use(req, res, next); expect(res.status).toHaveBeenCalledWith(401); }); });对于全局中间件可以直接测试HTTP请求it(应该允许白名单路由, async () { const app await Test.createTestingModule({ imports: [AppModule], }).compile(); app.use(ApiWhitelistMiddleware); const instance app.createNestApplication(); await instance.init(); const response await request(instance.getHttpServer()) .get(/api/login); expect(response.status).not.toBe(403); });8. 中间件与拦截器的区别很多新手会混淆中间件和拦截器。我在项目中也纠结过什么时候该用哪个。简单来说中间件更底层在请求进入NestJS路由之前处理拦截器更高层可以处理控制器输入输出一个实用的区分原则如果需要处理原始请求对象如修改headers用中间件如果需要处理业务数据用拦截器。比如我们要记录响应时间两种实现方式对比// 中间件方式 export class TimingMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const start Date.now(); res.on(finish, () { console.log(耗时: ${Date.now() - start}ms); }); next(); } } // 拦截器方式 Injectable() export class TimingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler) { const start Date.now(); return next.handle().pipe( tap(() { console.log(耗时: ${Date.now() - start}ms); }) ); } }中间件能获取更精确的时间包括路由匹配时间而拦截器只能测量控制器执行时间。根据需求选择合适的工具很重要。