@Next、Middleware、Interceptor
后端项目中,经常会有需要实现请求前后逻辑的情况,比如在请求前做 IP 验证,在请求后实现日志记录等等。
一个比较好的实践是将这些并不涉及单个业务逻辑的代码用 AOP 的形式从 Controller 逻辑中切割出来。
下面来谈谈 Nest.js 给出的实现方法:
- @Next 装饰器
- Middleware
- Interceptor
@Next 装饰器
这个装饰器主要用于调用下一个 handler 的。
在 Controller 中使用 @Next 装饰器来调用另外一个 handler 函数试试:
typescript1 @Get() 2 getHello1(@Next() next) { 3 console.log('before...'); 4 next(); 5 } 6 @Get() 7 getHello(@Next() next, @Response({ passthrough: true }) res: Res) { 8 console.log('handle...') 9 next(); 10 return 'Hello World!'; 11 } 12 @Get() 13 getHello2() { 14 console.log('after...'); 15 }
在同一个路由下,上面的代码执行过程是:
bash1before... 2handle... 3after...
单独使用@Next()情况下,Nest 不会处理返回值,因此最后一个 handler 的结果会被当做 response 返回。
这里是通过@Response({ passthrough: true })装饰器告知 Nest 把 getHello()的返回值作为响应。
Middleware
中间件是后端中最常用的选项。
跟 express.js 的中间件不同的地方在于, Nest.js 的中间件还支持注入 Provider。
因此在 Nestjs 中,我们通常会优先使用中间件来处理请求前后的一些逻辑。
生成中间件
bash1nest generate middleware test --no-spec
中间件代码,注入了 Service 依赖。
typescript1@Injectable() 2export class TestMiddleware implements NestMiddleware { 3 @Inject(AppService) 4 private readonly appService: AppService 5 async use(req: any, res: any, next: () => void) { 6 console.log('brefore') 7 const result = await this.appService.getHello() 8 console.log('——————🚀🚀🚀🚀🚀 —— result:', result) 9 next() 10 console.log('after') 11 } 12}
在 Module 中注册中间件:
ts1export class AppModule implements NestModule { 2 configure(consumer: MiddlewareConsumer) { 3 consumer.apply(TestMiddleware).forRoutes({ path: '/', method: RequestMethod.GET }) 4 } 5}
或者在 Module 中注册全局中间件:
typescript1export class AppModule implements NestModule { 2 configure(consumer: MiddlewareConsumer) { 3 consumer.apply(TestMiddleware).forRoutes('*') 4 } 5}
执行结果:
bash1brefore 2——————🚀🚀🚀🚀🚀 —— result: Hello World! 3handle... 4after
如果不想要注入依赖,也可以用纯函数,这样就不需要 class 了。
typescript1export class AppModule implements NestModule { 2 configure(consumer: MiddlewareConsumer) { 3 consumer 4 .apply((req, res, next) => { 5 console.log('...before') 6 next() 7 console.log('...after') 8 }) 9 .forRoutes('*') 10 } 11}
或者直接在入口文件使用 app.use 注册全局中间件(也是使用纯函数):
typescript1async function bootstrap() { 2 const app = await NestFactory.create(AppModule) 3 app.use((req, res, next) => { 4 console.log('...before') 5 next() 6 console.log('...after') 7 }) 8 await app.listen(3000) 9}
如果不需要注入依赖,那可以写函数形式的 middleware,这时候和 Express 的 middleware 就没啥区别了。
如果需要注入依赖,那就写 class 形式的 middleware,可以用 Nest 的依赖注入能力。
interceptor
生成 interceptor
bash1nest generate interceptor test --no-spec
生成代码
typescript1import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' 2import { Observable } from 'rxjs' 3 4@Injectable() 5export class TestInterceptor implements NestInterceptor { 6 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 7 return next.handle() 8 } 9}
使用 Interceptor
tsx1 @Get() 2 @UseInterceptors(TestInterceptor) 3 getHello(): string { 4 return this.appService.getHello(); 5 }
interceptor 如果借助 rx.js 的异步流式编程使用,能够实现获取、修改 request 或 response 等功能。
tap
下面是通过 tap 获取 response 的代码:
diff1@Injectable() 2export class TestInterceptor implements NestInterceptor { 3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 4- return next.handle() 5+ return next.handle().pipe(tap((value) => console.log('after...', value))); 6 } 7}
tap operator 不会改变数据,只是额外执行一段逻辑。
通过 tap 获取到 handler 结果,并且打印出来。访问http://localhost:3000/就可以看到结果:
bash1after... Hello World!
使用 tap operator 可以添加一些日志、缓存等逻辑。对于依赖注入的支持也使得实现这些功能更加便捷。
map
使用 map operator 来对 controller 返回的数据做一些修改。
typescript1@Injectable() 2export class TestInterceptor implements NestInterceptor { 3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 4 return next.handle().pipe( 5 map((data) => ({ 6 code: 200, 7 status: 'success', 8 data, 9 })), 10 ) 11 } 12}
catchError
controller 里很可能会抛出错误,这些错误会被 exception filter 处理,返回不同的响应,但在那之前,我们可以在 interceptor 里先处理下。
typescript1import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common' 2import { Observable, catchError, throwError } from 'rxjs' 3 4@Injectable() 5export class TestInterceptor implements NestInterceptor { 6 private readonly logger = new Logger(TestInterceptor.name) 7 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 8 return next.handle().pipe( 9 catchError((err) => { 10 this.logger.error(err.message, err.stack) 11 return throwError(() => err) 12 }), 13 ) 14 } 15}
timeout
接口如果长时间没返回,要给用户一个接口超时的响应,这时候就可以用 timeout operator。
typescript1@Injectable() 2export class TestInterceptor implements NestInterceptor { 3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 4 return next.handle().pipe( 5 timeout(3000), 6 catchError((err) => { 7 if (err instanceof TimeoutError) { 8 console.log(err) 9 return throwError(() => new RequestTimeoutException()) 10 } 11 return throwError(() => err) 12 }), 13 ) 14 } 15}
timeout 操作符会在 3s 没收到消息的时候抛一个 TimeoutError。
然后用 catchError 操作符处理下,如果是 TimeoutError,就返回 RequestTimeoutException,这个有内置的 exception filter 会处理成对应的响应格式。
其余错误就直接 throwError 抛出去。
RequestTimeoutException 的响应是这样的:
json1{ 2 "message": "Request Timeout", 3 "statusCode": 408 4}
全局 Interceptor 注入依赖
默认的全局的 Interceptor 是这样使用的:
typescript1import { NestFactory } from '@nestjs/core' 2import { AppModule } from './app.module' 3import { TestInterceptor } from './test/test.interceptor' 4 5async function bootstrap() { 6 const app = await NestFactory.create(AppModule) 7 await app.useGlobalInterceptors(new TestInterceptor()) 8 await app.listen(3000) 9} 10bootstrap()
但是这个用法没办法注入其他服务依赖。
Nest 提供了一个 token,用这个 token 在 AppModule 里声明的 interceptor,Nest 会把它作为全局 interceptor。
typescript1import { Module } from '@nestjs/common' 2import { AppController } from './app.controller' 3import { AppService } from './app.service' 4import { TestInterceptor } from './test/test.interceptor' 5import { APP_INTERCEPTOR } from '@nestjs/core' 6 7@Module({ 8 imports: [], 9 controllers: [AppController], 10 providers: [ 11 AppService, 12 { 13 provide: APP_INTERCEPTOR, 14 useClass: TestInterceptor, 15 }, 16 ], 17}) 18export class AppModule {}
现在就可以在全局 Interceptor 中注入其他 service 啦:

总结
后端项目中,经常需要做请求前、请求后的一些逻辑,比如 ip 验证(请求前)、记录日志(请求后)。
在 nest.js 中有三种方法可以做请求前和请求后的逻辑:
- @Next 装饰器
- Middleware
- Interceptor
这几种方法都可以注入依赖。
@Next 装饰器 不常用。
middleware 适合处理通用的逻辑。
interceptor 和 middleware 功能类似,但也有不同,interceptor 可以拿到目标 class、handler 等,也可以调用 rxjs 的 operator 来处理响应,更适合处理具体的业务逻辑。
常用的 operator 有以下几个:
- tap: 不修改响应数据,执行一些额外逻辑,比如记录日志、更新缓存等
- map:对响应数据做修改,一般都是改成 {code, data, message} 的格式
- catchError:在 exception filter 之前处理抛出的异常,可以记录或者抛出别的异常
- timeout:处理响应超时的情况,抛出一个 TimeoutError,配合 catchErrror 可以返回超时的响应
全局 interceptor 可以通过 APP_INTERCEPTOR 的 token 声明,这种能注入依赖,比 app.useGlobalInterceptors 更好。