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(); } }最后是请求转换中间件,它能预处理请求数据。比如我们把所有输入的空字符串转为null:
@Injectable() 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.get<AuthMiddleware>(AuthMiddleware); userService = module.get<UserService>(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`); }) ); } }中间件能获取更精确的时间(包括路由匹配时间),而拦截器只能测量控制器执行时间。根据需求选择合适的工具很重要。