淺淡 RxJS WebSocket

卡色發表於2018-09-26

引言

中後臺儀表盤是一個非常複雜,特別是當需要全面屏運用時,資料的實時性需求非常高。WebSocket 不管在什麼環境中使用其實都是非常簡單,各現代瀏覽器實現標準都很統一,而且介面也足夠簡單。

即便是在 Angular 也是如此,只需要簡單幾行程式碼就能使用 WebSocket。

const ws = new WebSocket('wss://echo.websocket.org');
ws.onmessage = (e) => {
  console.log('message', e);
}
複製程式碼

若需要向服務端傳送訊息,則:

ws.send(`content`);
複製程式碼

在 Angular 裡絕大多數的人都會根據上述程式碼進一步擴充,比如統一訊息解析、錯誤處理、多路複用等,並最終將其封裝成一個服務類。

事實上,RxJS 也包裹了一個 WebSocket Subject,位於 rxjs/websocket

如何使用

假如將上面的示例使用 RxJS 來寫,則:

import { webSocket, WebSocketSubject } from 'rxjs/webSocket';

const ws = webSocket('wss://echo.websocket.org');

ws.subscribe(res => {
  console.log('message', res);
});

ws.next(`content`);
複製程式碼

webSocket 是一個工廠函式,所生產出來的 WebSocketSubject 物件可被多次訂閱,若未訂閱或取消最後一個訂閱時都會導致 WebSocket 連線中斷,當再一次訂閱時會重新自動連線。

WebSocketSubjectConfig

webSocket 除了接收字串(WebSocket服務遠端地址)外,還允許指定更復雜的配置項。

預設情況下,訊息是使用 JSON.parseJSON.stringify 對訊息格式序列化和反序列化操作,所以不管訊息傳送或接收都以 JSON 為準,可通過 serializerdeserializer 屬性來改變。

若需要關心 WebSocket 什麼時候開始或結束(closeObserver),則:

const open$ = new Subject();
const ws = webSocket({
  url: 'wss://echo.websocket.org',
  openObserver: open$
});
// 訂閱開啟事件
open$.subscribe(() => {});
複製程式碼

訊息

WebSocketSubject 也是 Subject 的變體之一,因此訂閱它表示接收訊息,反之則利用 nextcompleteerror 來維護訊息的推送。

  • 使用 next 來傳送訊息
  • 使用 complete 會嘗試檢測是否最後一個訂閱,若是將會關閉連線
  • 使用 error 相當於原始 close 方法且必須提供 { code: number, reason?: string} 引數,注意 code 務必遵守取值範圍

可被重放

呼叫 next 傳送訊息時若 WebSocket 連線中斷(例如:沒人訂閱時),訊息會被快取當下一次重新連線以後會按順序傳送。這對於非同步世界裡非常方便,我們只需要確保 Angular 啟動前初始化好 WebSocket 不管什麼時候訂閱接收訊息,都可以隨時傳送也無須等待。

事實上這一點是 RxJS WebSocket 預設情況下是通過 webSocket 所生產的 WebSocketSubject 其本質上是 ReplaySubject 的“重放”能力。當然你可以通過 webSocket 的第二個引數改變這種行為。

多路複用

一般來說我們不太可能只會一個 Web Socket 服務完成所有的事,然而也不太可能針對每一個業務例項建立一個 webSocket。往往我們會增加一層閘道器並將這些業務 WebSocket 進行彙總,對於前端始終只需要一個連線,這就是多路複用存在的意義。

而核心是必須要讓後端知道,什麼時候傳送什麼訊息給什麼樣的服務。

首先必須先使用 multiplex 方法來建立 Observable 以便訂閱某一路訊息,它有三個引數來幫助我們區分訊息:

  • subMsg 告知正在訂閱哪一路訊息
  • unsubMsg 告知取消訂閱哪一路訊息
  • messageFilter 過濾訊息,使訂閱者只接收哪一路訊息
const ws = webSocket('wss://echo.websocket.org');
const user$ = this.ws.multiplex(
    () => ({ type: 'subscribe', tag: 'user' }),
    () => ({ type: 'unsubscribe', tag: 'user' }),
    message => message.type === 'user'
);
user$.subscribe(message => console.log(message));

const todo$ = this.ws.multiplex(
    () => ({ type: 'subscribe', tag: 'todo' }),
    () => ({ type: 'unsubscribe', tag: 'todo' }),
    message => message.type === 'todo'
);
todo$.subscribe(message => console.log(message));
複製程式碼

user$ 流和 todo$ 流他們共用一個 WebSocket 連線,這便是多路複用。

雖然訂閱是通過 multiplex 建立的,然後訊息的推送依然還是需要使用 ws.next()

總結

這原本是對內部一個簡單培訓,然而我發現竟然極少人會討論 RxJS 裡面 Web Socket 的實現。

其實一直有想著要給 ng-alain 內建 WebSocket,只是就封裝角度來講完全沒有價值,因為已經足夠優雅。

相關文章