Nest.js 是如何實現 AOP 架構的?

語言: CN / TW / HK

Nest.js 是一個 Node.js 的後端框架,它對 express 等 http 平臺做了一層封裝,解決了架構問題。它提供了 express 沒有的 MVC、IOC、AOP 等架構特性,使得程式碼更容易維護、擴充套件。

這裡的 MVC、IOC、AOP 都是啥意思呢?我們分別看一下:

MVC、IOC

MVC 是 Model View Controller 的簡寫。MVC 架構下,請求會先發送給 Controller,由它排程 Model 層的 Service 來完成業務邏輯,然後返回對應的 View。

Nest.js 提供了 @Controller 裝飾器用來宣告 Controller:

而 Service 會用 @Injectable 裝飾器來宣告:

通過 @Controller、@Injectable 裝飾器宣告的 class 會被 Nest.js 掃描,建立對應的物件並加到一個容器裡,這些所有的物件會根據構造器裡宣告的依賴自動注入,也就是 DI(dependency inject),這種思想叫做 IOC(Inverse Of Control)。

IOC 架構的好處是不需要手動建立物件和根據依賴關係傳入不同物件的構造器中,一切都是自動掃描並建立、注入的。

此外,Nest.js 還提供了 AOP (Aspect Oriented Programming)的能力,也就是面向切面程式設計的能力:

AOP

AOP 是什麼意思呢?什麼是面向切面程式設計呢?

一個請求過來,可能會經過 Controller(控制器)、Service(服務)、Repository(資料庫訪問) 的邏輯:

如果想在這個呼叫鏈路里加入一些通用邏輯該怎麼加呢?比如日誌記錄、許可權控制、異常處理等。

容易想到的是直接改造 Controller 層程式碼,加入這段邏輯。這樣可以,但是不優雅,因為這些通用的邏輯侵入到了業務邏輯裡面。能不能透明的給這些業務邏輯加上日誌、許可權等處理呢?

那是不是可以在呼叫 Controller 之前和之後加入一個執行通用邏輯的階段呢?

比如這樣:

這樣的橫向擴充套件點就叫做切面,這種透明的加入一些切面邏輯的程式設計方式就叫做 AOP (面向切面程式設計)。

AOP 的好處是可以把一些通用邏輯分離到切面中,保持業務邏輯的存粹性,這樣切面邏輯可以複用,還可以動態的增刪

其實 Express 的中介軟體的洋蔥模型也是一種 AOP 的實現,因為你可以透明的在外面包一層,加入一些邏輯,內層感知不到。

而 Nest.js 實現 AOP 的方式更多,一共有五種,包括 Middleware、Guard、Pipe、Inteceptor、ExceptionFilter:、

中介軟體 Middleware

Nest.js 基於 Express 自然也可以使用中介軟體,但是做了進一步的細分,分為了全域性中介軟體和路由中介軟體:

全域性中介軟體就是 Express 的那種中介軟體,在請求之前和之後加入一些處理邏輯,每個請求都會走到這裡:

路由中介軟體則是針對某個路由來說的,範圍更小一些:

這個是直接繼承了 Express 的概念,比較容易理解。

再來看一些 Nest.js 擴充套件的概念,比如 Guard:

Guard

Guard 是路由守衛的意思,可以用於在呼叫某個 Controller 之前判斷許可權,返回 true 或者 flase 來決定是否放行:

建立 Guard 的方式是這樣的:

Guard 要實現 CanActivate 介面,實現 canActive 方法,可以從 context 拿到請求的資訊,然後做一些許可權驗證等處理之後返回 true 或者 false。

通過 @Injectable 裝飾器加到 IOC 容器中,然後就可以在某個 Controller 啟用了:

Controller 本身不需要做啥修改,卻透明的加上了許可權判斷的邏輯,這就是 AOP 架構的好處。

而且,就像 Middleware 支援全域性級別和路由級別一樣,Guard 也可以全域性啟用:

Guard 可以抽離路由的訪問控制邏輯,但是不能對請求、響應做修改,這種邏輯可以使用 Interceptor:

Interceptor

Interceptor 是攔截器的意思,可以在目標 Controller 方法前後加入一些邏輯:

建立 Inteceptor 的方式是這樣的:

Interceptor 要實現 NestInterceptor 介面,實現 intercept 方法,呼叫 next.handle() 就會呼叫目標 Controller,可以在之前和之後加入一些處理邏輯。

Controller 之前之後的處理邏輯可能是非同步的。Nest.js 裡通過 rxjs 來組織它們,所以可以使用 rxjs 的各種 operator。

Interceptor 支援每個路由單獨啟用,只作用於某個 controller,也同樣支援全域性啟用,作用於全部 controller:

除了路由的許可權控制、目標 Controller 之前之後的處理這些都是通用邏輯外,對引數的處理也是一個通用的邏輯,所以 Nest.js 也抽出了對應的切面,也就是 Pipe:

Pipe

Pipe 是管道的意思,用來對引數做一些驗證和轉換:

建立 Pipe 的方式是這樣的:

Pipe 要實現 PipeTransform 介面,實現 transform 方法,裡面可以對傳入的引數值 value 做引數驗證,比如格式、型別是否正確,不正確就丟擲異常。也可以做轉換,返回轉換後的值。

內建的有 8 個 Pipe,從名字就能看出它們的意思:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

同樣,Pipe 可以只對某個路由生效,也可以對每個路由都生效:

不管是 Pipe、Guard、Interceptor 還是最終呼叫的 Controller,過程中都可以丟擲一些異常,如何對某種異常做出某種響應呢?

這種異常到響應的對映也是一種通用邏輯,Nest.js 提供了 ExceptionFilter 來支援:

ExceptionFilter

ExceptionFilter 可以對丟擲的異常做處理,返回對應的響應:

建立 ExceptionFilter的形式是這樣的:

首先要實現 ExceptionFilter 介面,實現 catch 方法,就可以攔截異常了,但是要攔截什麼異常還需要用 @Catch 裝飾器來宣告,攔截了異常之後,可以異常對應的響應,給使用者更友好的提示。

當然,也不是所有的異常都會處理,只有繼承 HttpException 的異常才會被 ExceptionFilter 處理,Nest.js 內建了很多 HttpException 的子類:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

當然,也可以自己擴充套件:

Nest.js 通過這樣的方式實現了異常到響應的對應關係,程式碼裡只要丟擲不同的 HttpException,就會返回對應的響應,很方便。

同樣,ExceptionFilter 也可以選擇全域性生效或者某個路由生效:

某個路由: 全域性:

我們瞭解了 Nest.js 提供的 AOP 的機制,但它們的順序關係是怎樣的呢?

幾種 AOP 機制的順序

Middleware、Guard、Pipe、Interceptor、ExceptionFilter 都可以透明的新增某種處理邏輯到某個路由或者全部路由,這就是 AOP 的好處。

但是它們之間的順序關係是什麼呢?

呼叫關係這個得看原始碼了。

對應的原始碼是這樣的:

很明顯,進入這個路由的時候,會先呼叫 Guard,判斷是否有許可權等,如果沒有許可權,這裡就拋異常了:

丟擲的 HttpException 會被 ExceptionFilter 處理。

如果有許可權,就會呼叫到攔截器,攔截器組織了一個鏈條,一個個的呼叫,最後會呼叫的 controller 的方法:

呼叫 controller 方法之前,會使用 pipe 對引數做處理:

會對每個引數做轉換:

ExceptionFilter 的呼叫時機很容易想到,就是在響應之前對異常做一次處理。

而 Middleware 是 express 中的概念,Nest.js 只是繼承了下,那個是在最外層被呼叫。

這就是這幾種 AOP 機制的呼叫順序。把這些理清楚,就算是對 Nest.js 有很好的掌握了。

總結

Nest.js 基於 express 這種 http 平臺做了一層封裝,應用了 MVC、IOC、AOP 等架構思想。

MVC 就是 Model、View Controller 的劃分,請求先經過 Controller,然後呼叫 Model 層的 Service、Repository 完成業務邏輯,最後返回對應的 View。

IOC 是指 Nest.js 會自動掃描帶有 @Controller、@Injectable 裝飾器的類,建立它們的物件,並根據依賴關係自動注入它依賴的物件,免去了手動建立和組裝物件的麻煩。

AOP 則是把通用邏輯抽離出來,通過切面的方式新增到某個地方,可以複用和動態增刪切面邏輯。

Nest.js 的 Middleware、Guard、Interceptor、Pipe、ExceptionFileter 都是 AOP 思想的實現,只不過是不同位置的切面,它們都可以靈活的作用在某個路由或者全部路由,這就是 AOP 的優勢。

我們通過原始碼來看了它們的呼叫順序,Middleware 是 Express 的概念,在最外層,到了某個路由之後,會先呼叫 Guard,Guard 用於判斷路由有沒有許可權訪問,然後會呼叫 Interceptor,對 Contoller 前後擴充套件一些邏輯,在到達目標 Controller 之前,還會呼叫 Pipe 來對引數做驗證和轉換。所有的 HttpException 的異常都會被 ExceptionFilter 處理,返回不同的響應。

Nest.js 就是通過這種 AOP 的架構方式,實現了松耦合、易於維護和擴充套件的架構。

AOP 架構的好處,你感受到了麼?