[NestJS]舉幾個栗子:攔截器篇

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

利用AOP能夠對業務邏輯的各個部分例如:權限控制,日誌統計,性能分析,異常處理等進行隔離,從而下降各部分的耦合度,提升程序的可維護性。sql

NestJS框架中體現AOP思想的部分有:Middleware(中間件), Guard(守衛器),Pipe(管道),Exception filter(異常過濾器)等,固然還有咱們今天的主角:Interceptor(攔截器)。express

使用方式

首先咱們看一下攔截器在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的執行時間,這是因爲程序的執行順序是 攔截器 =》路由執行 =》攔截器。app

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返回分頁列表總條數時,攔截器能夠將結果進行格式化:async

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,封裝自定義異常響應的裝飾器))。post

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添加額外功能
相關文章
相關標籤/搜索