AOP
(Aspect Oriented Programming),即面向切面编程,是 NestJS
框架中的重要内容之一。
利用 AOP
可以对业务逻辑的各个部分例如:权限控制,日志统计,性能分析,异常处理等进行隔离,从而降低各部分的耦合度,提高程序的可维护性。
NestJS
框架中体现 AOP
思想的部分有: Middleware
(中间件), Guard
(守卫器), Pipe
(管道), Exception filter
(异常过滤器)等,当然还有我们今天的主角: Interceptor
(拦截器)。
首先我们看一下拦截器在NestJS中的三种使用方式:
const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new SomeInterceptor()); 复制代码
@UseInterceptors(SomeInterceptor) export class SomeController {} 复制代码
export class SomeController { @UseInterceptors(SomeInterceptor) @Get() routeHandler(){ // 执行路由函数 } } 复制代码
下面我们通过一些例子来看一下拦截器具体有哪些使用场景:
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`)), ); } } 复制代码
我们知道,中间件也可以在路由执行之前添加额外的逻辑。而拦截器与中间件的主要区别之一就在于拦截器不只能路由执行之前,也能在执行之后添加逻辑。
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 }), ) } } 复制代码
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)来捕获特定的异常。
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
的逻辑。
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
。
通过以上几个例子,我们可以总结出拦截器的几个作用:
routeHandler
执行之前或之后添加额外的逻辑 routeHandler
的返回结果进行转化 routeHandler
抛出的异常进行处理 routeHandler
的行为 routeHandler
,为 routeHandler
添加额外功能