在 NestJS 的世界裡,微服務應用程式(Microservices) 被定義為:使用與 HTTP 協定不同傳輸層的應用程式。
這個定義很容易誤解,原因是當我們在談論微服務如何跨服務溝通時,最簡單也最容易實現的方式即透過 HTTP 協定來溝通,那麼在 NestJS 使用 RESTful API 進行跨服務溝通,難道就不是微服務架構了嗎?事實上仍是微服務架構,但 該服務本身 在 NestJS 的定義是 HTTP 應用程式 (HTTP Server)。
基於微服務應用程式的定義,NestJS 提供了一些傳輸層實作,這些實作稱為 傳輸器(Transporter),透過 Transporter 可以讓 客戶端(Client) 輕易地與微服務應用程式進行溝通。
Transporter 是溝通的渠道,要解決的問題是「要透過哪種方式將訊息傳遞給其他服務」,比如說:NATS、gRPC 等。用現實生活中的例子來說明的話,Transporter 就像是 messenger、Line 等通訊軟體,無論用哪一種,重點都在於「把想表達的事情傳遞給他人」。渠道的選擇也是需要仔細評估的一環,試想如果要密集討論一件事情,用 email 可能就不是一個好選擇,或是對方沒有使用 email 的習慣,那就會導致這個溝通無法進行。
NestJS 在 Transporter 下了許多功夫,盡可能地抽象介面,讓開發者能使用相同的開發體驗來實現不同 Transporter 下的實作,甚至在抽換 Transporter 時,也能以最小改動幅度來進行調整,這對開發者來說可說是一大福音。
有了 Transporter 作為溝通的渠道,還需要有處理訊息的方法,NestJS 以 模式(Pattern) 的方式來識別訊息,它是一串文字或是可序列化的物件,如:order.created
、{ cmd: 'hello' }
等。客戶端會將 Pattern 與內容透過指定的 Transporter 傳輸到微服務應用程式,其需以相同的方式來解析訊息,如此一來,可以很輕易地針對特定訊息做處理。用現實生活的例子來說明的話,Pattern 就像描述一件事情的關鍵資訊,假如有位朋友傳訊息只傳了「100」這個數字,我不會知道他在說什麼,但如果說:「可以借我錢嗎?100元」這樣就很清楚知道要如何做回應。
透過 NestCLI 產生一個專案:
$ nest new <PROJECT_NAME>
專案產生完之後,需額外安裝微服務應用程式相關套件:
$ npm install @nestjs/microservices
安裝完畢後,透過修改載入點 main.ts
的內容,來建立 NestJS 微服務應用程式:
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: 3333,
},
},
);
await app.listen();
}
bootstrap();
仔細觀察,與 HTTP Server 的差異僅在於使用不同的方法來實例化 NestJS App,微服務應用程式使用 NestFactory.createMicroservice
進行實例化,這個方法可帶入兩個參數,第一個參數為 根模組 (Root Module),第二個參數是一個 options
物件,可以指定 Transporter 以及該 Transporter 相關的設置。
補充:不指定 Transporter 會直接以 TCP Transporter 做為預設值,後續的章節會針對其他不同的 Transporter 做更詳細的說明。
上方範例指定使用的 Transporter 為 TCP Transporter,它有下列四個屬性可以設定:
host
:要連線的主機,如:localhost
。port
:指定要連線的主機 port,如:3333
。retryAttempts
:當無法連至主機時的重試次數,預設是 0
。retryDelay
:每次重試的間隔時間,以毫秒(ms)為單位,預設是 0
。注意:由於各個 Transporter 基本概念都差不多,只有細節功能會有差異,所以下方皆會以 TCP Transporter 的角度來介紹。
一切準備好就可以透過下方指令啟動應用程式:
$ npm run start:dev
大部分的 Transporter 支援兩種訊息模式:請求-回應模式(Request-response) 與 事件本位模式(Event-based)。
這種訊息模式被廣泛用來 交換(Exchange) 訊息,可以確保每個請求都有回應,是非常常見的模式。不過須特別注意,NestJS 為了要監聽請求與回應訊息,需要消耗較多的資源,所以 不建議 用此模式處理不需要回應訊息的情境。
下方是範例程式碼,在 AppController
內的 sayHello
方法添加 @MessagePattern
裝飾器,並將 Pattern 設定為 { cmd: 'hello' }
,NestJS 會偵測套用 @MessagePattern
的方法與設置的 Pattern,讓該方法成為指定 Pattern 的 處理程式(Handler):
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
sayHello(data: string): string {
return `Hello, ${data}`;
}
}
注意:
@MessagePattern
裝飾器只有在 Controller 使用才有效,原因是監聽來自外部傳入的訊息,性質上較接近 Controller。
以上方的範例來說,符合 Pattern { cmd: 'hello' }
的訊息會被導到 sayHello
方法,該方法的參數僅有一個 data
,所以其他服務要發送訊息時,僅會帶一個字串作為該訊息的 Payload。
在多數情境中,會需要非同步操作,比如:讀取資料庫,這時可以使用 ES7 的 async/await
進行處理:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
async sayHello(data: string) {
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve(`Hello, ${data}`);
}, 100);
});
return result;
}
}
在某些情境下,可能會希望以串流的方式回應多個值,直到某個條件達成,這時可以使用 RxJS 來處理,將 Observable
回傳 NestJS 會自動訂閱,直到該 Observable
進入 complete
狀態:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { from } from 'rxjs';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'important' })
sayThreeTimes(data: string) {
return from([`${data}!`, `${data}!`, `${data}!`]);
}
}
提醒:RxJS 在處理非同步與串流等情境十分好用,不熟悉的朋友可以參考 Mike 大大寫的「打通 RxJS 任督二脈」系列文。
這種訊息模式適合用在 非交換訊息 的情境,客戶端單方面向微服務應用程式發送事件與相關訊息,微服務應用程式不需進行回應。
下方是範例程式碼,在 AppController
內的 onOrderCreated
方法添加 @EventPattern
裝飾器,並將 Pattern 設定為 order.created
,NestJS 會偵測套用 @EventPattern
的方法與設置的 Pattern,讓該方法成為指定 Pattern 的 Handler:
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
onOrderCreated(order: { name: string; }) {
console.log(order);
}
}
NestJS 支援相同事件註冊多個 Handler,這些 Handler 在收到事件時會並行處理。下方是範例程式碼,sendEmail
以及 sendNotification
將會同時收到 Pattern 為 order.created
的事件與相關訊息:
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
sendEmail(order: { name: string; }) {
console.log('send Email');
}
@EventPattern('order.created')
sendNotification(order: { name: string; }) {
console.log('send notification');
}
}
在某些情境下,可能會需要取得該請求的相關資訊,以 TCP Transporter 來說,可以透過 TcpContext
來獲取 Socket 等資訊,但如果是使用別的 Transporter 則會有不一樣的資訊可以獲取。
那麼要如何取得 Context 呢?需要針對 Handler 做一些調整,本來取得 Payload 的方式是直接在 Handler 設置一個參數,現在因為要多獲取 Context,所以需要在 Payload 的參數前面添加 @Payload
裝飾器、在 Context 的參數前面添加 @Ctx
裝飾器。下方是範例程式碼:
import { Controller } from '@nestjs/common';
import {
Ctx,
MessagePattern,
Payload,
TcpContext,
} from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
onOrderCreated(
@Payload() order: { name: string; },
@Ctx() ctx: TcpContext
) {
console.log(ctx);
console.log(order);
}
}
NestJS 所定義的微服務應用程式是使用與 HTTP 協定不同傳輸層的應用程式,為了滿足不同的傳輸方式,NestJS 設計了 Transporter,讓開發者可以使用相同的開發體驗來實作,降低因轉換 Transporter 所產生的成本。大多數的 Transporter 支援 Request-response 與 Event-based 訊息模式,透過 Request-response 可以確保每個發送的請求都一定會有回應,適合用來交換訊息,但如果只是要接收訊息不回應,使用 Event-based 會是比較好的選擇。
今天的內容是著重在微服務應用程式本身,那用 NestJS 該如何實作客戶端呢?明天的內容將會解答給各位,敬請期待!