STOMP ist ein aufregend einfach und dennoch leistungsfähiges Protokoll für die Sendung von Nachrichten, das von populären Servern wie RabbitMQ, ActiveMQ und Apollo implementiert wird. Die Verwendung von STOMP über WebSocket ist ein direktes Protokoll, was es zu einer populären Wahl für die Sendung von Nachrichten aus einem Web-Browser macht, da Protokolle wie AMQP durch Browser begrenzt werden, die TCP-Verbindungen nicht zulassen.

Um mit STOMP über WebSocket zu arbeiten, kann man @stomp/stompjs verwenden, das jedoch schwierige Callbacks und eine komplizierte API aufweist, die auf spezialisierteren Anwendungsfällen abzielen. Glücklicherweise gibt es auch das weniger bekannte @stompjs/rx-stomp, das über ein nettes Interface via RxJS Observablen bietet. Observablen sind nicht nur für Angular verpflichtet und passen gut zu wie React funktioniert. Es ist eine nette Schnittstelle, wenn man komplexe Workflows und Pipelines mit vielen verschiedenen Nachrichtenquellen zusammensetzt.

Der Tutorial folgt einem etwas ähnlichen Pfad wie der ursprünglichen Version in Angular, aber die Komponentenstruktur und der Code-Stil sind auf den funktionalen Stil von React abgestimmt.

Hinweis: Dieses Tutorial ist mit strict TypeScript geschrieben, aber der JavaScript-Code ist fast identisch, da wir nur 5 Type-Deklarationen haben. Für die JS-Version kann man die Typ-Imports und -Definitionen überspringen.

Inhaltsverzeichnis

Ziele

Hier wird ein vereinfachter Chatroom-Anwendung erstellt, die verschiedene Aspekte von RxStomp in verschiedenen Komponenten aufzeigt. Insgesamt möchten wir:

  • Ein React-Frontend, das mit RxStomp über einen STOMP-Server verbunden ist.

  • Ein lebendiger Verbindungsstatusanzeige basierend auf unserer Verbindung mit dem STOMP-Server.

  • Pub/Sub-Logik für jede konfigurierbare Thema.

  • Teilen von RxStomp-Logik über mehrere Komponenten, um zu zeigen, wie Logik und Verantwortung getrennt werden können.

  • RxStomp-Verbindungen/Abonnement-Lifecycles mit den React-Komponenten-Lifecycles einheitlich positionieren, um sicherzustellen, dass keine Verluste oder offene Beobachter auftreten.

Voraussetzungen

  • Sie sollten einen STOMP-Server ausführen, damit die React-Anwendung mit ihm verbunden werden kann. Hier verwenden wir RabbitMQ mit der rabbitmq_web_stomp-Erweiterung.

  • Neueste React-Version. In diesem Lehrbuch verwenden wir v18, obwohl ältere Versionen möglicherweise ebenfalls funktionieren.

  • Einige Erfahrung mit Observablen wird auch hilfreich sein.

STOMP-Server-Starter mit RabbitMQ

Wenn Sie RabbitMQ auch verwenden möchten (nicht unbedingt erforderlich), hier sind Installationsanleitungen für verschiedene Betriebssysteme. Um die Erweiterung hinzuzufügen, müssen Sie ausführen:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Wenn Sie Docker verwenden können, wird ein Docker-File ähnlich zu diesem alles, was für das Lehrbuch notwendig ist, einrichten:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Start-Template React

Für dieses Tutorial verwenden wir das Vite-Template `react-ts`. Der Kern unserer Anwendung wird im `App`-Komponenten sein, und wir werden Kindkomponenten für andere spezifische STOMP-Funktionen erstellen.

Wie man RxStomp installiert

Wir werden das npm-Paket @stomp/rx-stomp verwenden:

$ npm i @stomp/rx-stomp rxjs

Dadurch wird Version 2.0.0 installiert.

Hinweis: Dieses Tutorial funktioniert ohne ausdrückliche Angabe von rxjs weil es ein Schwesterprodukt ist, aber es ist eine gute Praxis, es explizit anzugeben.

Wie man die Verbindung und die Trennung vom STOMP-Server verwaltet

Nun öffnen wir `App.tsx` und initialisieren unseren `RxStomp`-Client. Da der Client kein Zustand ist, der sich für die Darstellung ändern wird, schließen wir ihn mit dem `useRef`-Hook ein.

// 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

Vorausgesetzt, die Standardports und Authentifizierungsdetails, definieren wir nächstes eine Konfiguration für unsere Verbindung.

// 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() {
  ...

Für eine bessere Entwickler Erfahrung haben wir alle Nachrichten mit Zeitstempel in die lokale Konsole geloggt und setzten niedrige Timerfrequenzen. Ihre Konfiguration sollte für Ihre Produktionsanwendung völlig anders sein, deshalb lesen Sie die RxStompConfig Dokumentation für alle verfügbaren Optionen durch.

Nächstes geht es daran, die Konfiguration in einem useEffect-Hook an rxStomp weiterzugeben. Dies verwaltet die Aktivierung der Verbindung zusammen mit dem Lebenszyklus des Komponenten.

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

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

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

Obwohl es in unserer App keine visuelle Änderung gibt, sollten die Logs die Verbindungs- und Pingspezifikation anzeigen. Hier ist ein Beispiel dafür:

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

Hinweis: Im Allgemeinen kann die Duplikation von Logs ein Zeichen dafür sein, dass die Deaktivierung oder die Unabhängigkeitsfunktion nicht korrekt implementiert wurde. React rendert in der Entwicklungsumgebung jeden Komponenten zweimal, um diese Bugs mit React.StrictMode zu erkennen.

Wie man den Verbindungsstatus überwachen kann

RxStomp verfügt über ein RxStompState enum, das die möglichen Verbindungszustände mit unserem Broker repräsentiert. Unser nächstes Ziel besteht darin, den Verbindungsstatus in unserer UI anzuzeigen.

Lassen Sie uns ein neues Komponenten für diesen Zweck namens Status.tsx erstellen:

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

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

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

Wir können das Observable rxStomp.connectionState$ verwenden, um uns an unseren connectionStatus-String zu binden. Analog zu dem Weise, wie wir useEffect verwendet haben, werden wir die Ausführung beenden, um unsubscribe() zu调用.

// 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>
    </>
  )
}

Um es anzuzeigen, fügen wir es unserer App hinzu:

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

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

An diesem Punkt sollten Sie ein funktionierendes visuelles Indikator auf dem Bildschirm haben. Versuchen Sie, das STOMP-Server herunterzufahren und zu sehen, ob die Protokolle wie erwartet funktionieren.

Wie man Nachrichten sendet

Lassen Sie uns einfache Chatroom erstellen, um einen vereinfachten End-zu-End-Nachrichtenfluss mit dem Broker zu zeigen.

Wir können die Funktionalität in einem neuen Chatroom-Komponenten platzieren. Zuerst können wir die Komponente mit einem benutzerdefinierten username– und message-Feld erstellen, das mit Eingaben verbunden ist.

// 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'
      />
    </>
  )    
}

Lassen Sie uns dies innerhalb unserer App mit einem Schalter zum Beitreten ins Chatroom integrieren:

// 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}/>
        </>
      )}

    </>
  )

Es ist nun Zeit, tatsächlich Nachrichten zu senden. STOMP ist am besten geeignet, um textbasierte Nachrichten zu senden (binäre Daten sind auch möglich). Wir werden die Struktur der Daten, die wir senden, in einer neuen types-Datei definieren:

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

Hinweis: Wenn Sie TypeScript nicht verwenden, können Sie diese Typdefinition überspringen.

Nächstes, verwenden wir JSON, um die Nachricht zu serialisieren und senden wir Nachrichten an unser STOMP-Server mit dem .publish-Methode, einem Zielthema und unserem JSON-body-Inhalt.

// 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>
    </>
  )
}

Um es auszuprobieren, klicken Sie ein paar Mal auf die Nachricht senden-Schaltfläche und sehen, ob die Serialisierung funktioniert. Obwohl Sie zunächst keine visuellen Änderungen sehen können, sollten die Konsoleprotokolle es anzeigen:

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

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

Wie man Empfängt Nachrichten

Wir erstellen eine neue Komponente, um die Liste der Nachrichten von allen Benutzern anzuzeigen. Zunächst verwenden wir den gleichen Typ, übergeben den Themenamen als Prop und zeigen alles als Liste an. All dies geht in eine neue Komponente namens 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>
  </>
  )
}

Es ist Zeit, alles zusammenzufügen!

Ähnlich wie bei der Verwaltung des Abonnements mit der Status-Komponente, richten wir das Abonnement bei der Montage ein und abonnieren es bei der Demontage ab.

Mit RxJS pipe und map können wir unser JSON wieder in unser ChatMessage deserialisieren. Das modulare Design ermöglicht es Ihnen, eine kompliziertere Pipeline mit RxJS-Operatoren einzurichten, wenn erforderlich.

// 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()
    }
  }, [])

  ...

Zu diesem Zeitpunkt sollte die Chat-Oberfläche Nachrichten korrekt anzeigen und Sie können experimentieren, indem Sie mehrere Registerkarten als verschiedene Benutzer öffnen.

Ein weiterer Versuch hier ist, den STOMP-Server auszuschalten, einige Nachrichten zu senden und ihn wieder einzuschalten. Die Nachrichten sollten lokal in der Warteschlange und sobald der Server bereit ist, weitergeleitet werden. Nett!

Zusammenfassung

In diesem Tutorial haben wir:

  • @stomp/rx-stomp installiert, um eine schöne Entwicklererfahrung zu ermöglichen.

  • RxStompConfig eingerichtet, um unseren Client mit den Verbindungsdetails, Debugger-Protokollierung und Timer-Einstellungen zu konfigurieren.

  • Verwendete rxStomp.activate und rxStomp.deactivate, um den Hauptlebenszyklus des Clients zu verwalten.

  • Überwachte den Abonnementsstatus mithilfe des rxStomp.connectionState$ Observables.

  • Veröffentlichte Nachrichten mit rxStomp.publish an konfigurierbare Ziele und Nachrichteninhalte.

  • Erstellte ein Observable für ein bestimmtes Thema mit rxStomp.watch.

  • Verwendete sowohl Konsolenprotokolle als auch React-Komponenten, um die Bibliothek in Aktion zu sehen und die Funktionalität sowie Fehlertoleranz zu überprüfen.

Sie können den finalen Code auf Gitlab finden: https://gitlab.com/harsh183/rxstomp-react-tutorial. Fühlen Sie sich frei, ihn auch als Startervorlage zu verwenden und melden Sie eventuelle Probleme, die auftreten können.