@Next、Middleware、Interceptor

后端项目中,经常会有需要实现请求前后逻辑的情况,比如在请求前做 IP 验证,在请求后实现日志记录等等。

一个比较好的实践是将这些并不涉及单个业务逻辑的代码用 AOP 的形式从 Controller 逻辑中切割出来。

下面来谈谈 Nest.js 给出的实现方法:

  1. @Next 装饰器
  2. Middleware
  3. Interceptor

@Next 装饰器

这个装饰器主要用于调用下一个 handler 的。

在 Controller 中使用 @Next 装饰器来调用另外一个 handler 函数试试:

typescript
1  @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  }

在同一个路由下,上面的代码执行过程是:

bash
1before...
2handle...
3after...

单独使用@Next()情况下,Nest 不会处理返回值,因此最后一个 handler 的结果会被当做 response 返回。

这里是通过@Response({ passthrough: true })装饰器告知 Nest 把 getHello()的返回值作为响应。

Middleware

中间件是后端中最常用的选项。

跟 express.js 的中间件不同的地方在于, Nest.js 的中间件还支持注入 Provider。

因此在 Nestjs 中,我们通常会优先使用中间件来处理请求前后的一些逻辑。

生成中间件

bash
1nest generate middleware test --no-spec

中间件代码,注入了 Service 依赖。

typescript
1@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 中注册中间件:

ts
1export class AppModule implements NestModule {
2  configure(consumer: MiddlewareConsumer) {
3    consumer.apply(TestMiddleware).forRoutes({ path: '/', method: RequestMethod.GET })
4  }
5}

或者在 Module 中注册全局中间件:

typescript
1export class AppModule implements NestModule {
2  configure(consumer: MiddlewareConsumer) {
3    consumer.apply(TestMiddleware).forRoutes('*')
4  }
5}

执行结果:

bash
1brefore
2——————🚀🚀🚀🚀🚀 —— result: Hello World!
3handle...
4after

如果不想要注入依赖,也可以用纯函数,这样就不需要 class 了。

typescript
1export 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 注册全局中间件(也是使用纯函数):

typescript
1async 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

bash
1nest generate interceptor test --no-spec

生成代码

typescript
1import { 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

tsx
1  @Get()
2  @UseInterceptors(TestInterceptor)
3  getHello(): string {
4    return this.appService.getHello();
5  }

interceptor 如果借助 rx.js 的异步流式编程使用,能够实现获取、修改 request 或 response 等功能。

tap

下面是通过 tap 获取 response 的代码:

diff
1@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/就可以看到结果:

bash
1after... Hello World!

使用 tap operator 可以添加一些日志、缓存等逻辑。对于依赖注入的支持也使得实现这些功能更加便捷。

map

使用 map operator 来对 controller 返回的数据做一些修改。

typescript
1@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 里先处理下。

typescript
1import { 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。

typescript
1@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 的响应是这样的:

json
1{
2  "message": "Request Timeout",
3  "statusCode": 408
4}

全局 Interceptor 注入依赖

默认的全局的 Interceptor 是这样使用的:

typescript
1import { 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。

typescript
1import { 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 啦:

image-20231106220941776

总结

后端项目中,经常需要做请求前、请求后的一些逻辑,比如 ip 验证(请求前)、记录日志(请求后)。

在 nest.js 中有三种方法可以做请求前和请求后的逻辑:

  1. @Next 装饰器
  2. Middleware
  3. 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 更好。