iT邦幫忙

2024 iThome 鐵人賽

DAY 2
0
Software Development

用 NestJS 闖蕩微服務!系列 第 2

[用NestJS闖蕩微服務!] DAY02 - 初探微服務應用程式(上)

  • 分享至 

  • xImage
  •  

NestJS 微服務應用程式概念

在 NestJS 的世界裡,微服務應用程式(Microservices) 被定義為:使用與 HTTP 協定不同傳輸層的應用程式

這個定義很容易誤解,原因是當我們在談論微服務如何跨服務溝通時,最簡單也最容易實現的方式即透過 HTTP 協定來溝通,那麼在 NestJS 使用 RESTful API 進行跨服務溝通,難道就不是微服務架構了嗎?事實上仍是微服務架構,但 該服務本身 在 NestJS 的定義是 HTTP 應用程式 (HTTP Server)

基於微服務應用程式的定義,NestJS 提供了一些傳輸層實作,這些實作稱為 傳輸器(Transporter),透過 Transporter 可以讓 客戶端(Client) 輕易地與微服務應用程式進行溝通。

NestJS Microservices Concept

傳輸器 (Transporter)

Transporter 是溝通的渠道,要解決的問題是「要透過哪種方式將訊息傳遞給其他服務」,比如說:NATS、gRPC 等。用現實生活中的例子來說明的話,Transporter 就像是 messenger、Line 等通訊軟體,無論用哪一種,重點都在於「把想表達的事情傳遞給他人」。渠道的選擇也是需要仔細評估的一環,試想如果要密集討論一件事情,用 email 可能就不是一個好選擇,或是對方沒有使用 email 的習慣,那就會導致這個溝通無法進行。

NestJS 在 Transporter 下了許多功夫,盡可能地抽象介面,讓開發者能使用相同的開發體驗來實現不同 Transporter 下的實作,甚至在抽換 Transporter 時,也能以最小改動幅度來進行調整,這對開發者來說可說是一大福音。

模式 (Pattern)

有了 Transporter 作為溝通的渠道,還需要有處理訊息的方法,NestJS 以 模式(Pattern) 的方式來識別訊息,它是一串文字或是可序列化的物件,如:order.created{ cmd: 'hello' } 等。客戶端會將 Pattern 與內容透過指定的 Transporter 傳輸到微服務應用程式,其需以相同的方式來解析訊息,如此一來,可以很輕易地針對特定訊息做處理。用現實生活的例子來說明的話,Pattern 就像描述一件事情的關鍵資訊,假如有位朋友傳訊息只傳了「100」這個數字,我不會知道他在說什麼,但如果說:「可以借我錢嗎?100元」這樣就很清楚知道要如何做回應。

NestJS Pattern Concept

前置作業

透過 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 訊息模式

大部分的 Transporter 支援兩種訊息模式:請求-回應模式(Request-response)事件本位模式(Event-based)

Request-response

這種訊息模式被廣泛用來 交換(Exchange) 訊息,可以確保每個請求都有回應,是非常常見的模式。不過須特別注意,NestJS 為了要監聽請求與回應訊息,需要消耗較多的資源,所以 不建議 用此模式處理不需要回應訊息的情境。

NestJS Request-response message style

下方是範例程式碼,在 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 任督二脈」系列文。

Event-based

這種訊息模式適合用在 非交換訊息 的情境,客戶端單方面向微服務應用程式發送事件與相關訊息,微服務應用程式不需進行回應。

NestJS Event-based message style

下方是範例程式碼,在 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');
  }
}

取得 Payload 與 Context

在某些情境下,可能會需要取得該請求的相關資訊,以 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 該如何實作客戶端呢?明天的內容將會解答給各位,敬請期待!


上一篇
[用NestJS闖蕩微服務!] DAY01 - 簡介
下一篇
[用NestJS闖蕩微服務!] DAY03 - 初探微服務應用程式(下)
系列文
用 NestJS 闖蕩微服務!21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言