阅读 698

[NestJS]举几个栗子:拦截器篇

AOP(Aspect Oriented Programming),即面向切面编程,是NestJS框架中的重要内容之一。

利用AOP可以对业务逻辑的各个部分例如:权限控制,日志统计,性能分析,异常处理等进行隔离,从而降低各部分的耦合度,提高程序的可维护性。

NestJS框架中体现AOP思想的部分有:Middleware(中间件), Guard(守卫器),Pipe(管道),Exception filter(异常过滤器)等,当然还有我们今天的主角:Interceptor(拦截器)。

使用方式

首先我们看一下拦截器在NestJS中的三种使用方式:

1. 全局绑定

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new SomeInterceptor());
复制代码

2. 绑定到控制器

@UseInterceptors(SomeInterceptor)
export class SomeController {}
复制代码

3. 绑定到特定路由上

export class SomeController {
    @UseInterceptors(SomeInterceptor)
    @Get()
    routeHandler(){
        // 执行路由函数
    }
}
复制代码

使用场景

下面我们通过一些例子来看一下拦截器具体有哪些使用场景:

1. 在routeHandler执行之前或之后添加额外的逻辑:LoggingInterceptor

下面这个例子可以计算出routeHandler的执行时间,这是由于程序的执行顺序是 拦截器 =》路由执行 =》拦截器。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    // 保存路由执行前的时间
    const now = Date.now();
    return next
      .handle()
      .pipe(
        // 计算出这个路由的执行时间
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}
复制代码

我们知道,中间件也可以在路由执行之前添加额外的逻辑。而拦截器与中间件的主要区别之一就在于拦截器不只能路由执行之前,也能在执行之后添加逻辑。

2. 对routeHandler的返回结果进行转化: PaginateInterceptor

下面例子中,我们展示了拦截器的另一个重要应用,对返回的结果进行转化。当routeHandler返回分页列表总条数时,拦截器可以将结果进行格式化:

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Request } from 'express'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

@Injectable()
export class PaginateInterceptor implements NestInterceptor {
    public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(
            // 规定返回的数据格式必须为[分页列表,总条数]
            map((data: [any[], number]) => {
                const req: Request = context.switchToHttp().getRequest()
                const query = req.query
                // 判断是否一个分页请求
                const isPaginateRequest = req.method === 'GET' && query.current && query.size
                // 判断data是否符合格式
                const isValidData = Array.isArray(data) && data.length === 2 && Array.isArray(data[0])

                if (isValidData && isPaginateRequest) {
                    const [list, total] = data
                    return {
                        data: list,
                        meta: { total, size: query.size, current: query.current },
                        status: 'succ',
                    }
                }
                return data
            }),
        )
    }
}
复制代码

3. 对routeHandler抛出的异常进行处理: TypeormExceptionInterceptor

如果你使用的ORM是TypeOrm的话,也许你会接触过TypeOrm抛出的EntityNotFoundError异常。这个异常是由于sql语句执行时找不到对应的行时抛出的错误。

在下面的例子里拦截器捕获到了TypeOrm抛出的EntityNotFoundError异常后,改为抛出我们自定义的EntityNoFoundException(关于自定义异常,可参考另一篇文章基于@nestjs/swagger,封装自定义异常响应的装饰器))。

import { EntityNoFoundException } from '@common/exception/common.exception'
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable, throwError } from 'rxjs'
import { catchError } from 'rxjs/operators'

@Injectable()
export class TypeOrmExceptionInterceptor implements NestInterceptor {
    public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(
            catchError(err => {
                if (err.name === 'EntityNotFound') {
                    return throwError(new EntityNoFoundException())
                }
                return throwError(err)
            }),
        )
    }
}
复制代码

看到这里,各位看官可能有个疑问:拦截器和异常过滤器有什么差别? 首先,时机不同,拦截器的执行顺序在异常过滤器之前,这意味着拦截器抛出的错误,最后可经由过滤器处理;其次,对象不同,拦截器捕获的是routeHandler抛出的所有异常,而异常过滤器可通过@Catch(SomeException)来捕获特定的异常。

4. 在特定条件下,完全重写routeHandler的行为:CacheInterceptor

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}
复制代码

这里例子里,当命中缓存时,通过return of([]);语句直接返回了结果,而不走routeHandler的逻辑。

5. 拓展routeHandler,为routeHandler添加额外功能:BindRoleToUserInterceptor

在业务上,有时我们需要在用户调用某些接口后,对用户执行一些额外操作,比如添加标签,或者添加角色。这个时候,就可以通过拦截器来实现这个功能。下面这个例子里,拦截器发挥实现是在某个接口调用成功后,给用户绑定上角色的功能,

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { User } from '@src/auth/user/user.entity'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
import { getConnection } from 'typeorm'

/**
 * 用于给用户绑定角色
 */
@Injectable()
export class BindRoleToUserInterceptor implements NestInterceptor {
    public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(
            tap(async () => {
                const req = context.switchToHttp().getRequest()
                await this.bindRoleToUser(req.roleId, req.user.id)
            }),
        )
    }
    
    /**
     * 这里假定用户和角色是多对多的关系,此处省略User表和Role表的结构
     */
    public async bindRoleToUser(roleId: number, userId: number) {
        await getConnection()
            .createQueryBuilder()
            .relation(User, 'roles')
            .of(userId)
            .add(roleId)
    }
}
复制代码

当有多个接口都有类似逻辑的时候,使用拦截器就实现代码的复用,并与接口的主要功能分隔开,实现AOP

总结

通过以上几个例子,我们可以总结出拦截器的几个作用:

  1. routeHandler执行之前或之后添加额外的逻辑
  2. routeHandler的返回结果进行转化
  3. routeHandler抛出的异常进行处理
  4. 在特定条件下,完全重写routeHandler的行为
  5. 拓展routeHandler,为routeHandler添加额外功能