本文成文時間是2019-08-18
,文中提到的最新版本號是以2019-08-18
為基準的。
前情摘要
在介紹正文之前需要先簡單瞭解幾個概念: STOMP
協議、STOMP over WebSocket
以及 RxJS
。(關於這三點本文不會進行詳細介紹)
什麼是 STOMP?
STOMP 即 Simple or Streaming Text Orientated Messageing Protocal ,是一種簡單(流)文字定向傳輸協議。
STOMP 是 WebSocket 更高階的子協議,它使用一個基於幀的格式來定義訊息,與 HTTP 的 Request
和 Response
類似。
STOMP 提供可互操作的連線格式,允許 STOMP 客戶端與任意代理進行互動。STOMP 是一個非常簡單易用的協議, 伺服器端實現起來會相對困難一些,編寫客戶端非常容易。
STOMP over WebSocket
STOMP over Websocket 即通過 WebSocket 建立 STOMP 連線,也就是說是在 WebSocket 連線的基礎上再建立 STOMP 連線。
WebSocket 協議定義了兩種型別的訊息,文字和二進位制,但它們的內容是未定義的。
如果說 Socket 是 C/S 架構 的 TCP 程式設計,那麼同理 WebSocket 就是 B/S架構的 TCP 程式設計,所以需要在客戶端與服務端之間定義一個機制去協商一個子協議 - 更高階別的訊息協議,將它使用在 WebSocket 之上去定義每次傳送訊息的類別、格式和內容,等等。
子協議的使用是可選的,但無論哪種方式,客戶端和伺服器都需要就一些定義訊息內容的協議達成一致。於是,通常選擇在 WebSocket 協議上使用 STOMP 協議來定義內容格式。
RxJS
RxJS 是一個用於使用 Observables 進行反應式程式設計的庫,可簡化編寫非同步或基於回撥的程式碼的過程。該專案是對 Reactive-Extensions / RxJS 的重寫,具有更好的效能,更好的模組化,更好的可除錯呼叫堆疊,同時主要保持向後相容,並且進行了一些重大更改,從而減少了 API 操作。
RxJS 是 Reactive Extensions for JavaScript 的縮寫,起源於 Reactive Extensions(Rx),Rx 是對 LINQ 的一種擴充套件,他的目標是對非同步的集合進行操作,也就是說,集合中的元素是非同步填充的,比如說從 Web 或者雲端獲取資料然後對集合進行填充。
LINQ(Language Integrated Query)語言整合查詢是一組用於 C# 和 Visual Basic 語言的擴充套件。它允許編寫 C# 或者 Visual Basic 程式碼以操作記憶體資料的方式,查詢資料庫。
RxJS 的主要功能是利用響應式程式設計的模式來實現 JavaScript 的非同步式程式設計(現前端主流框架 Vue React Angular 都是響應式的開發框架)。
RxJS 是基於觀察者模式和迭代器模式以函數語言程式設計思維來實現的。學習 RxJS 之前我們需要先了解觀察者模式和迭代器模式,還要對 Stream 流的概念有所認識。
接下來我們就一起來看下如何在的 Angular 專案中是使用 STOMP over WebSocket
進行資料流傳輸的。
Angular 實戰
本文案例是實際 Angular 專案中的一個小功能模組,Angular 是 8.0 版本,本文涉及的元件主要包括右鍵選單項負責生產訊息的context-menu-component
動態元件,進度監控app-progress-bar
元件和日誌輸出元件app-console-area
。
專案中使用的 UI 框架為 ng-zorro-antd,下面是 tabs 元件中的相關虛擬碼(省略了元件間 Input Ouput 介面):
...
<nz-tab [nzType]="'card'">
<ng-template #consoleArea>控制檯</ng-template>
<app-progress-bar></app-progress-bar>
<app-console-area></app-console-area>
</nz-tab>
...
程式碼與 UI 檢視的對應關係如下:
專案中使用的 STOMP 客戶端是 ng2-stompjs
庫, ng2-stompjs
目前的版本是 7.xx
,其底層的 @stomp/stompjs
已被重寫,自此與 STOMP 標準具有嚴格的相容性。
ng2-stompjs 是第一個可靠地支援二進位制有效負載的 STOMP JS 客戶端庫。
安裝 ng-stompjs
:
$ npm install @stomp/ng-stompjs
新增和注入 @stomp/ng2-stompjs
使用前需要定義配置檔案,在目錄 src/app/config/
建立 stomp.config.js 檔案:
import { InjectableRxStompConfig } from '@stomp/ng2-stompjs';
import { STOMP_SERVER_BASE_URL } from 'server.config';
const _window: any = window;
export const myRxStompConfig: InjectableRxStompConfig = {
// Which server?
brokerURL: _window.STOMP_SERVER_BASE_URL
? _window.STOMP_SERVER_BASE_URL
: STOMP_SERVER_BASE_URL
// Headers
// Typical keys: login, passcode, host
connectHeaders: {
login: 'guest',
passcode: 'guest'
},
// How often to heartbeat?
// Interval in milliseconds, set to 0 to disable
heartbeatIncoming: 0, // Typical value 0 - disabled
heartbeatOutgoing: 20000, // Typical value 20000 - every 20 seconds
// Wait in milliseconds before attempting auto reconnect
// Set to 0 to disable
// Typical value 500 (500 milli seconds)
reconnectDelay: 200,
// Will log diagnostics on console
// It can be quite verbose, not recommended in production
// Skip this key to stop logging to console
debug: (msg: string): void => {
console.log(new Date(), msg);
}
};
在建立例項時,此配置將由 Angular Dependency Injection 機制注入 RxStompService 服務,在 src/app/app.module.ts
檔案中,新增以下內容。
import { InjectableRxStompConfig, RxStompService, rxStompServiceFactory } from '@stomp/ng2-stompjs';
import { myRxStompConfig } from './config/stomp.config';
...
@NgModule({
declarations: [/* 宣告模組內部成員的地方 */],
imports: [/* 匯入的其他module */],
providers: [
{
provide: InjectableRxStompConfig,
useValue: myRxStompConfig
},
{
provide: RxStompService,
useFactory: rxStompServiceFactory,
deps: [InjectableRxStompConfig]
}
],
entryComponents: [/* 不會在模版中引用到的元件 */],
bootstrap: [AppComponent]
})
export class AppModule {}
建立連線
我們現在將 RxStompService 依賴注入 app-progress-bar
元件中,為此我們將它新增到建構函式中,如下所示:
constructor(private rxStompService: RxStompService) { }
為了能實時接收伺服器傳送過來的訊息,我們需要在 app-progress-bar
元件的生命週期函式 OnInit
中,使用 watch()
方法進行訂閱:
ngOnInit() {
// 訂閱 STOMP 訊息
this.topicSubscription = this.rxStompService.watch('/topic/message').subscribe((message: Message) => {
console.log(message.body);
}
this.errorSubscription = this.rxStompService.watch('/topic/error').subscribe((message: Message) => {
this.progressInfo = message.body;
});
}
注:app-message-bar 元件預設是不顯示的,當有訊息傳遞進來時,此元件才會顯示在頁面中,進度達到 100% 時,會自動隱藏。
STOMP 協議是如何將訊息準確傳送的目的地的呢?
文章開頭提到,STOMP 是一種基於幀的協議,其幀在 HTTP 上建立模型。一個框架由一個命令,一組可選的標題和一個可選的主體組成。
STOMP 伺服器被建模為可以向其傳送訊息的一組目標,STOMP 協議將目標視為不透明字串,其語法是特定於伺服器實現的。另外,STOMP 沒有定義目的地 destination 的傳遞語義應該是什麼。目的地的傳遞或「訊息交換」語義可能因伺服器而異,甚至從目的地到目的地也不同,這使得伺服器可以使用 STOMP 支援的語義進行創作。
STOMP 客戶端是一個使用者代理,可以在兩種(可能是同時的)模式下執行:
- 作為生產者,通過 SEND 框架將訊息傳送到伺服器上的目的地。
- 作為消費者,傳送 SUBSCRIBE 給定目的地的幀並從伺服器接收訊息作為MESSAGE 幀。
我們的案例中兩種模式同時存在,傳送訊息的是生產者(我們上文提到的 context-menu-component
動態元件),接收訊息的是消費者(app-progress-bar
元件)。消
費者可以通過訂閱不同的 destination,來獲得不同的推送訊息,不需要開發人員去管理這些訂閱與推送目的地之前的關係。
接下來就介紹下作為生產者的 context-menu-component
元件,看看它都做了哪些事情吧。
傳送訊息
context-menu-component
元件是觸發右鍵時動態產生的元件,它負責通過向不同的目的地 destination 下達不同的指令,進而來實現不同的功能需求。
使用 ng-zorro-antd
的 Dropdown
元件 ,動態生成:
public openProjectManagerContextMenu(context: ProjectManagerContext): void {
this.contextMenuComponent = this.nzDropdownService.create(context.mouseEvent, this.contextMenuTemplate);
}
當我們點選「執行用例」按鈕時,它作為生產者會向 STOMP 服務端目的地 SEND 訊息指令。
// 執行用例
public runProjectCases(): void {
const streamTaskParam: StreamTaskParam = new StreamTaskParam();
streamTaskParam.project = this.globalService.projectInfo.projectName;
this.openTaskProgressModal('/app/run-project-cases', JSON.stringify(streamTaskParam));
}
從程式碼得知,這會將訊息傳送到名為的 /app/run-project-cases
的目的地,STOMP 將此目標視為不透明字串,並且目標名稱不承擔傳遞語義。
STOMP 定義了自己的訊息傳輸體制。首先是通過一個後臺繫結的連線點 endpoint 來建立 socket 連線,然後生產者通過 SEND 方法,繫結好傳送的 destination。而 topic 和 app 則是一種訊息處理手段的分支,走 app/url 的訊息會被你設定到的 MassageMapping 攔截到,進行你自己定義的具體邏輯處理,而走 topic/url 的訊息就不會被攔截,直接到 Simplebroker 節點中將訊息推送出去。(其中 simplebroker 是 spring 的一種基於記憶體的訊息佇列,你也可以使用 activeMQ,rabbitMQ 代替)。
因此目的地 /app/run-project-cases
生產出來的訊息會被攔截,最終會轉發到消費者 app-progress-bar
元件 的 /topic/message
。
接收訊息
app-progress-bar
元件作為消費者使用 watch()
方法啟動與代理的訂閱,this.rxStompService.watch('/topic/message')
將代理到目的地為 /topic/message
的訂閱上,並返回 RxJS Observable。
ngOnInit() {
// 訂閱 STOMP 訊息
this.topicSubscription = this.rxStompService.watch('/topic/message').subscribe((message: Message) => {
console.log(message.body);
// do something
}
}
app-progress-bar
元件都做了些什麼事情呢?它負責建立 STOMP 連線,從伺服器端接收文字流,並將這些流進行資料解析,解析出來的資料一部分用來控制進度條的數值變化,一部分用來控制 app-console-area
元件日誌的輸出節點。
也就是說 app-console-area
元件中列印的內容是由 app-progress-bar
元件解析和傳遞的。
取消訂閱
我們知道 RxJS Observable 實際上就是一個函式,它接收一個 Observer 物件作為引數,返回一個函式用來取消訂閱。所以我們可以在 app-progress-bar
元件銷燬時,呼叫 unsubscribe()
方法取消訂閱。
ngOnDestroy() { this.topicSubscription.unsubscribe();}
本文主要目的是是結合案例展現 STOMP 協議的使用場景,所以不會著重介紹案例上的功能以及實現細節。
「記一次」系列文章:
相關文獻:
- STOMP over WebSocket http://jmesnil.net/stomp-webs...
- ng2-stompjs - https://github.com/stomp-js/n...
- RxJS - https://rxjs-dev.firebaseapp....
- ng-zorro-antd - https://ng.ant.design/docs/in...