STOMPは、RabbitMQ、ActiveMQ、Apolloなどの人気のサーバーで実装されている、信頼性のある送信メッセージのための非常にシンプルで強力なプロトコルです。WebSocketを介したSTOMPを使用することで、AMQPのようなプロトコルとしては、TCP接続を許可しないブラウザーに制約されることなく、ウェブブラウザからのメッセージ送信に人気のある選択肢になります。

STOMP over WebSocketを使用するには、@stomp/stompjsを使用することができますが、これには厄介なコールバックと複雑なAPIがあり、より専門的な使用形態に合わせています。幸いにも、より有名ではない@stompjs/rx-stompがあり、RxJSの観察可能な接口を提供しています。観察可能な概念はAngular独自ではなく、Reactでの動作にも非常に似ています。これは、異なるメッセージ源が多数含まれる複雑なワークフローやパイプラインの作曲にIDEFに似ています。

このチュートリアルは、Angularの最初のバージョンと似たようなパスを進めますが、コンポーネント構造とコードスタイルは、Reactの機能的スタイルに調整されています。

注:このチュートリアルはstrict TypeScriptで書かれていますが、JS版のコードは几乎同じであるため、型のインポートと定義をスキップすることができます。

目次

目標

ここで、RxStompのさまざまな側面を異なるコンポーネントで示す简化されたチャットルームアプリケーションを作成します。全体的な目標は以下の通りです。

  • ReactフロントエンドをSTOMPサーバーにRxStompと接続します。

  • STOMPサーバーとの接続状態に基づいたライブ接続状態表示です。

  • 任意の設定可能なトピックに対するPub/Subロジックです。

  • RxStompロジックを複数のコンポーネントに分割し、ロジックと責任の分離方法を示します。

  • RxStompの接続/購読ライフサイクルをReactコンポーネントのライフサイクルと合わせることで、漏洩や未閉じられた观的察者を Ensureします。

前提条件

  • Reactアプリケーションが接続できるSTOMPサーバーを実行していること。ここでは、rabbitmq_web_stompエクステンションを使用してRabbitMQを使用します。

  • 最新のReactバージョン。このチュートリアルではv18を使用しますが、古いバージョンもおそらく機能します。

  • 观的察者についての少しの熟悉さが役立つこともあります。

RabbitMQを使用したSTOMPサーバーのスターター

RabbitMQも使用したい場合は(厳密に必要ではありません)、以下は異なるオペレーティングシステムについてのインストールガイドです。エクステンションを追加するには、以下のコマンドを実行する必要があります:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Dockerを使用できる場合、このようなDockerファイルのDockerをここに似せることで、チュートリアルに必要なすべてのものを設定できます:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

スターター リアクト テンプレート

このチュートリアルでは、Vitereact-tsテンプレートを使用します。私たちのアプリケーションの中心部分はAppコンポーネントにあり、他の特定のSTOMP機能について子コンポーネントを作成します。

RxStompのインストール方法

ここでは、@stomp/rx-stomp npmパッケージを使用します。

$ npm i @stomp/rx-stomp rxjs

これにより、2.0.0バージョンがインストールされます。

注意: このチュートリアルはrxjsを明示的に指定しなくても機能しますが、それを明示的にするのは良い慣習です。

STOMPサーバーとの接続と切断の管理方法

では、App.tsxを開いて、RxStompクライアントを初期化しましょう。クライアントは渲染に影響を与えない状態ではありませんので、useRefフックに包んでいます。

// src/App.tsx
import { useRef } from 'react'
import { RxStomp } from '@stomp/rx-stomp'

import './App.css'

function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  return (
    <>
      <h1>Hello RxStomp!</h1>
    </>
  )
}

export default App

デフォルトのポートと認証詳細を想定して、次に接続のための設定を定義します。

// src/App.tsx

import { RxStomp } from '@stomp/rx-stomp'
import type { RxStompConfig } from '@stomp/rx-stomp'
...
const rxStompConfig: RxStompConfig = {
  brokerURL: 'ws://localhost:15674/ws',
  connectHeaders: {
    login: 'guest',
    passcode: 'guest',
  },
  debug: (msg) => {
    console.log(new Date(), msg)
  },
  heartbeatIncoming: 0,
  heartbeatOutgoing: 20000,
  reconnectDelay: 200,
}

function App() {
  ...

開発体验を向上させるために、すべてのメッセージに時間スタンプをつけてローカルコンソールに出力し、タイマーの周波数を低く設定しました。您的設定はプロダクションアプリケーションには大きく異なるため、RxStompConfig 文档を参照して利用可能なすべてのオプションを確認してください。

次に、useEffectフック内でrxStompに設定を渡します。これにより、コンポーネントライフサイクルとともに接続のアクティベーションが管理されます。

// src/App.tsx
...
function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  useEffect(() => {
    rxStomp.configure(rxStompConfig)
    rxStomp.activate()

    return () => { 
      rxStomp.deactivate() 
    }
  })
  ...

アプリに視覚的な変化はありませんが、ログを確認すると接続とピンのログが表示されるはずです。以下はその例です。

Date ... >>> CONNECT
login:guest
passcode:guest
accept-version:1.2,1.1,1.0
heart-beat:20000,0

Date ... Received data 
Date ... <<< CONNECTED
version:1.2
heart-beat:0,20000
session:session-EJqaGQijDXqlfc0eZomOqQ
server:RabbitMQ/4.0.2
content-length:0

Date ... connected to server RabbitMQ/4.0.2 
Date ... send PING every 20000ms 
Date ... <<< PONG 
Date ... >>> PING

注意: 一般的に、重複したログが表示される場合は、非アクティベーションや購読解除機能が正しく実装されていない可能性があります。React.StrictModeを使用して、Reactは開発環境で各コンポーネントを2回レンダリングし、人々がこれらのバグを発見しやすくします。

接続ステータスを監視する方法

RxStompには、ブローカーとの可能な接続状態を表すRxStompState enumがあります。次の目標は、UIに接続ステータスを表示することです。

これのためにStatus.tsxという新しいコンポーネントを作成しましょう。

// src/Status.tsx
import { useState } from 'react'

export default function Status() {
  const [connectionStatus, setConnectionStatus] = useState('')

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

rxStomp.connectionState$オブザーバブルを使用して、connectionStatus文字列にバインドできます。useEffectを使用した方法と同様に、アンマウントアクションでunsubscribe()を使用します。

// src/Status.tsx
import { RxStompState } from '@stomp/rx-stomp'
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'


export default function Status(props: { rxStomp: RxStomp }) {
  const [connectionStatus, setConnectionStatus] = useState('')

  useEffect(() => {
    const statusSubscription = props.rxStomp.connectionState$.subscribe((state) => {
      setConnectionStatus(RxStompState[state])
    })

    return () => {
      statusSubscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

それを表示するために、アプリに含めます。

// src/App.tsx
import Status from './Status'
...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>
    </>
  )

この時点で、お使いのスクリーンには機能するビジュアルインジケータが表示されているはずです。STOMPサーバーをダウンにしてみて、ログが预期通りに機能しているか確認してください。

メッセージの送信方法

ブローカーを使用した簡単なチャットルームを作成し、エンドツーエンドのメッセージングフローを簡略化して表示しましょう。

機能を新しいChatroomコンポーネントに配置することができます。まず、入力に绑定于ける独自のusernamemessageフィールドをコンポーネントに作成することができます。

// src/Chatroom.tsx
import { useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  const [message, setMessage] = useState('')
  const [userName, setUserName] = useState(`user${Math.floor(Math.random() * 1000)}`)

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor='username'>Username: </label>
      <input
        type='text'
        name='username'
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor='message'>Message: </label>

      <input
        type='text'
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name='message'
      />
    </>
  )    
}

これをAppに含め、チャットルームに参加するためのトグルを追加しましょう。

// src/App.tsx
import { useEffect, useState, useRef } from 'react'
import Chatroom from './Chatroom'
...
function App() {
  const [joinedChatroom, setJoinedChatroom] = useState(false)
  ...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>

      {!joinedChatroom && (
        <button onClick={() => setJoinedChatroom(true)}>
          Join chatroom!
        </button>
      )}

      {joinedChatroom && (
        <>
          <button onClick={() => setJoinedChatroom(false)}>
            Leave chatroom!
          </button>

          <Chatroom rxStomp={rxStomp}/>
        </>
      )}

    </>
  )

実際のメッセージ送信の時間です。STOMPはテキストベースのメッセージ(バイナリデータも可能です)を送信するには最適です。送信するデータの構造を新しいtypesファイルで定義しましょう。

// types.ts
interface ChatMessage {
  userName: string,
  message: string
}

注:TypeScriptを使用していない場合は、この型定義を追加する必要はないです。

次に、JSONを使用してメッセージを序列化し、.publishを使用してデスクトップトピックとJSONbodyでSTOMPサーバーにメッセージを送信しましょう。

// src/Chatroom.tsx
import type { ChatMessage } from './types'
...
const CHATROOM_NAME = '/topic/test'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  ...
  function sendMessage(chatMessage: ChatMessage) {
    const body = JSON.stringify({ ...chatMessage })
    props.rxStomp.publish({ destination: CHATROOM_NAME, body })
    console.log(`Sent ${body}`)
    setMessage('')
  }

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor="username">Username: </label>
      <input
        type="text"
        name="username"
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor="message">Message: </label>

      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name="message"
      />

      <button onClick={() => sendMessage({userName, message})}>Send Message</button>
    </>
  )
}

テストのために、送信ボタンをいくつかクリックして、序列化が正しいか確認してください。まだビジュアル変更は見られないかもしれませんが、コンソールログはそれを示すはずです。

Date ... >>> SEND
destination:/topic/test
content-length:45

Sent {"userName":"user722","message":"1234567890"}

メッセージの受信方法

新しいコンポーネントを作成して、すべてのユーザーからのメッセージリストを表示します。今のところ、同じ型を使用し、トピック名をプロップとして渡し、すべてをリストとして表示します。これらはすべて、MessageListという新しいコンポーネントに入れます。

// src/MessageDisplay.tsx
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'
import type { ChatMessage } from './types'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  return(
  <>
  <h2>Chat Messages</h2>
  <ul>
    {chatMessages.map((chatMessage, index) => 
      <li key={index}>
        <strong>{chatMessage.userName}</strong>: {chatMessage.message}
      </li>
    )}
  </ul>
  </>
  )
}

すべてをまとめる時間です!

Statusコンポーネントと同様に、マウント時にサブスクリプションを設定し、アンマウント時に解除します。

RxJSのpipemapを使用して、JSONをChatMessageにデシリアライズできます。モジュール型のデザインでは、必要に応じてRxJS演算子を使用してより複雑なパイプラインを設定できます。

// src/MessageDisplay.tsx
...
import { map } from 'rxjs'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  useEffect(() => {
    const subscription = props.rxStomp
      .watch(props.topic)
      .pipe(map((message) => JSON.parse(message.body)))
      .subscribe((message) => setChatMessages((chatMessages) => [...chatMessages, message]))

    return () => {
      subscription.unsubscribe()
    }
  }, [])

  ...

この時点で、チャットGUIはメッセージを正しく表示するはずです。異なるユーザーとして複数のタブを開いて実験してみてください。

ここで試してみたいことは、STOMPサーバーをオフにして、いくつかのメッセージを送信し、再度オンにすることです。メッセージはローカルにキューに入れられ、サーバーが準備ができたらディスパッチされるはずです。素晴らしい!

まとめ

このチュートリアルでは:

  • 快適な開発体験のために@stomp/rx-stompをインストールしました。

  • 接続詳細、デバッガーログ、タイマー設定でクライアントを構成するためにRxStompConfigを設定しました。

  • クライアントの主要なライフサイクルを管理するために、rxStomp.activateおよびrxStomp.deactivateを使用しました。

  • rxStomp.connectionState$ observableを使用して購読状態を監視しました。

  • rxStomp.publishを使用して、構成可能な宛先とメッセージ本文でメッセージを発行しました。

  • rxStomp.watchを使用して、与えられたトピックに対応するobservableを作成しました。

  • コンソールログとReactコンポーネントを使用して、ライブラリの動作を確認し、機能と不具合の Toleranceを確認しました。

最終的なコードはGitlabにあります: https://gitlab.com/harsh183/rxstomp-react-tutorial。それをスターターテンプレートとして使用し、発生する可能な問題を報告する的自由にお願いいたします。