Nest 中的中间件本质上是在路由处理程序之前调用的函数。允许开发者对请求或响应对象进行操作。
由于 Nest 底层默认使用 Express 作为 HTTP 服务器,因此 Nest 的中间件默认情况下等价于 express 的中间件。所以,我们先来了解一下 express 的中间件。
Express 中间件
Express 的中间件基于洋葱模型,use
方法使用 req
、res
、next
作为参数,依赖调用 next()
来执行下一个中间件。
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('中间件 1');
next();
});
app.use((req, res, next) => {
console.log('中间件 2');
next();
});
// 处理请求
app.get('/', (req, res) => {
res.send('Hello Express');
});
app.listen(3000);
Nest 中间件的创建和使用
Nest 的中间件分为类和函数两种形式。
类中间件
创建
创建 NestMiddleware 类
使用 nest cli 快速创建
nest g middleware test
执行命令后,在 src/test 下创建 test.middleware.ts 文件:
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class TestMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
next();
}
}
可以看出,Nest 中间件类需要实现 NestMiddleware
接口,且被 @Injectable()
装饰。
与 express 类型对应
因为 nest cli 不知道我们在 Nest 底层使用 express 还是 fastify,所以 req
、res
参数的类型都是 any
。我们修改一下它们的类型:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TestMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
next();
}
}
这时 Nest 中间件和 Express 中间件完全等价了:
- 实现
use
方法。 use
方法的req
、res
、next
参数完全来自于 Express API。use
方法中可以执行任何代码,比如对请求、响应对象进行操作等。- 必须调用
next()
将控制权传递给下一个中间件函数,否则请求将会被挂起。
依赖注入
Nest 中间件通过 @Injectable
装饰器声明这个类被 IoC 容器接管,可以通过依赖注入使用其他 service。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private configService: ConfigService) {}
use(req: Request, res: Response, next: NextFunction) {
const logLevel = this.configService.get<string>('LOG_LEVEL') || 'info';
console.log(`[${logLevel}] Request: ${req.method} ${req.url}`);
next();
}
}
注册
注册中间件的模块需要实现 NestModule
接口,然后使用 configure()
方法来进行注册。
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TestMiddleware).forRoutes('*');
}
}
MiddlewareConsumer
是一个辅助类,它提供了几种内置方法来管理中间件,这些方法都可以使用链式调用的方式组织在一起。比如 apply
、 forRoutes
、 exclude
。
apply
apply()
方法用于注册中间件,可以接受单个中间件,也可以接受多个中间件。
consumer.apply(TestMiddleware).forRoutes('*');
consumer.apply(TestMiddleware, TestMiddleware2).forRoutes('*');
forRoutes
forRoutes()
方法用于定义中间件对哪些路由生效,可以接受的参数类型如下:
接受字符串(单个或多个)
表示路由路径。
// 表示 TestMiddleware 仅适用于 /user 路由
app.use(TestMiddleware).forRoutes('user');
// 表示 TestMiddleware 适用于 `/user` 和 `/order`
app.use(TestMiddleware).forRoutes('user', 'order');
接受 RouteInfo 对象
用于精确指定路由,包括方法和路径。
// 表示 TestMiddleware 适用于 `GET /user` 和 `POST /order`
app.use(TestMiddleware).forRoutes(
{ path: 'user', method: RequestMethod.GET },
{ path: 'order', method: RequestMethod.POST },
);
接受控制器类(单个或多个)
表示应用到某个或多个控制器上的所有路由。
app.use(TestMiddleware).forRoutes(UserController);
app.use(TestMiddleware).forRoutes(UserController, OrderController);
路由通配符
forRoutes()
方法传入的字符串和 RouteInfo
对象的 path
属性上都可以使用路由通配符,比如:
// 匹配所有路由
app.use(TestMiddleware).forRoutes('*');
// 匹配 abcd、ab_cd、abecd 等等。
app.use(TestMiddleware).forRoutes({ path: "ab*cd", method: RequestMethod.ALL });
字符 ""、"+"、"*"、"()" 都可以在路径中使用,它们是对应于正则表达式的子集。
exclude
exclude()
方法用于排除特定的路由不使用中间件。
// TestMiddleware 适用于 /user 及其子路由,但不会应用于 /user/login
app.use(TestMiddleware)
.exclude('user/login')
.forRoutes('user');
exclude()
方法和 forRoutes()
一样也可以接受 RouteInfo
对象。
app.use(TestMiddleware)
.exclude({ path: 'user/login', method: RequestMethod.POST })
.forRoutes('user');
函数中间件
创建
也可以使用普通函数作为中间件,函数的参数和类中间件 use
方法的参数一致,接受 req
、res
、next
参数。
import { Request, Response, NextFunction } from 'express';
export function testMiddleware(req: Request, res: Response, next: NextFunction) {
console.log('Request');
next();
}
注册
函数中间件的注册方式和类中间件一样,在 configure
方法中使用 apply
注册即可。
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TestMiddleware).forRoutes('*');
}
}
如何选择类和函数形式
上面介绍了 Nest 中间件的两种呢形式,那么应该如何选择呢?
一般来说,类中间件使用更多。因为类中间件中支持依赖注入,使中间件更具有扩展性,更适合大型项目,也与 Nest 的核心思想更加相符。
路由中间件和全局中间件
Nest 中间件的应用范围可以是特定的路由也可以是全局。
特定路由的中间件使用 MiddlewareConsumer
上的 apply
方法注册, forRoutes
方法定义适用路由,上面已经有过相关例子。
全局范围的中间件即针对所有路由生效,通过在 main.ts 文件中使用 app.use() 方法注册。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TestMiddleware } from './middleware/testt.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局中间件
app.use(TestMiddleware);
await app.listen(3000);
}
bootstrap();
Nest 中间件的使用场景
Nest 中间件用于在路由处理程序之前执行某些逻辑,以及处理请求和响应。常见的使用场景如下:
日志
比如记录请求信息等。
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();
}
}
身份验证
在请求到达路由处理程序之前,验证用户身份或权限,并记录用户信息
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const decoded = jwt.verify(token, 'your-secret-key');
req.user = decoded;
next();
} catch (err) {
throw new UnauthorizedException('Invalid token');
}
}
}
请求修改
比如在请求上下文上增加额外信息。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
req.requestId = uuidv4(); // 为每个请求生成唯一的 ID
next();
}
}
响应修改
比如缓存响应数据,有缓存则直接返回。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
const cache = new Map<string, any>();
@Injectable()
export class CacheMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const cacheKey = req.originalUrl;
if (cache.has(cacheKey)) {
return res.json(cache.get(cacheKey));
}
next();
}
}
还有其他很多场景,这里就不一一举例了。
总结
-
Nest 中间件的本质是一个函数。
-
Nest 的中间件和Express 的中间件默认情况下是等价的,都实现了
use
方法,接受req
、res
、next
参数,且通过调用next()
执行下一个中间件函数。二者最大的区别是,Nest 中间件支持依赖注入。 -
Nest 中间件分为 class 形式和 function 形式。二者最大的区别也是是否支持依赖注入。
-
Nest 注册中间件时,可以针对全局,也可以针对特定路由。针对特定路由时,可以通过字符串、
RouteInfo
对象或控制器指定,也可以使用通配符。 -
Nest 中间件有多种使用场景,包括日志、身份验证、请求响应的修改等。