STOMP is een opvallend eenvoudige yet krachtige protocol voor het verzenden van berichten, geimplementeerd door populaire servers zoals RabbitMQ, ActiveMQ en Apollo. Het gebruik van STOMP over WebSocket is eenvoudig protocol, waardoor het een populaire keuze is voor het verzenden van berichten uit een webbrowser, omdat protocollen zoals AMQP beperkt zijn door browsers die geen TCP-verbindingen toestaan.

Om STOMP over WebSocket te gebruiken, kun je @stomp/stompjs gebruiken, maar dat heeft moeilijke terugkoppelingen en een ingewikkeld API dat meer gekozen maakt voor specifieke scenario’s. gelukkig is er ook de minder bekende @stompjs/rx-stomp die een mooi interface biedt via RxJS observables. Observables zijn niet exclusief aan Angular verbonden en ze passen goed bij hoe React werkt. Het is een nette interface als je complexe workflow’s en pijplijnen samenstelt met veel verschillende berichtbronnen.

Het handleiding volgt op een soortgelijke manier als de eerste versie in Angular, maar de componentstructuur en code stijl zijn geconfigureerd voor de functionale stijl van React.

Opmerking: Deze handleiding is geschreven met strict TypeScript, maar het JavaScript-code is bijna identiek omdat we enkel 5 type declaraties hebben. Voor de JS-versie kun je de type importeren en definities overslaan.

Inhoudsopgave

Doelen

Hier zullen we een vereenvoudigde chatroom-toepassing bouwen die verschillende aspecten van RxStomp laat zien in verschillende componenten. Over het algemeen willen we het volgende hebben:

  • Een React-frontend verbonden met RxStomp naar een STOMP-server.

  • Een live weergave van de verbindingsstatus op basis van onze verbinding met de STOMP-server.

  • Pub/Sub-logica voor een configureerbaar onderwerp.

  • Het opsplitsen van RxStomp-logica over meerdere componenten om te laten zien hoe logica en verantwoordelijkheid kunnen worden gescheiden.

  • RxStomp-verbindingen/abonnees levenscyclus aanpassen aan React componentenlevenscycli om te verzekeren dat er geen lekken of niet-gesloten waarschuwers zijn.

Vereisten

  • U moet een STOMP-server draaien zodat de React-toepassing erop kan verbinden. Hier gebruiken we RabbitMQ met de extensie rabbitmq_web_stomp.

  • Nieuwste React-versie. In deze handleiding wordt versie v18 gebruikt, hoewel oudere versies waarschijnlijk ook goed zullen werken.

  • Een voorbereiding op het begrip van observabelen zal ook helpen.

Starter STOMP Server met RabbitMQ

Als u ook RabbitMQ wilt gebruiken (niet strikt verplicht), zijn er installatiehandleidingen voor verschillende besturingssystemen. Om de extensie toe te voegen, moet u de volgende opdracht uitvoeren:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Als u Docker kunt gebruiken, zal een Docker-bestand dat lijkt op dit alles in orde maken voor de handleiding:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Starttemplate React

Voor deze handleiding zullen we Vite’s react-ts sjabloon gebruiken. Het centraal deel van onze applicatie zal in het App-component zijn, en we zullen childcomponents aanmaken voor andere specifieke STOMP-functionaliteit.

Hoe RxStomp te installeren

We zullen de npm-pakket @stomp/rx-stomp gebruiken:

$ npm i @stomp/rx-stomp rxjs

Dit zal versie 2.0.0 installeren.

Opmerking: Deze handleiding werkt nog steeds zonder expliciet rxjs te specificeren, want dat is een zusterafhankelijkheid, maar het is goed om er expliciet over te spreken.

Hoe verbinding en afsluiten met de STOMP-server te beheren

Nu gaan we App.tsx openen en onze RxStomp-client initialiseren. Omdat de client geen status is die zal veranderen voor het renderen, zullen we hem in de useRef-hook afdekken.

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

Houdende rekening met de standaard poorten en authenticatiedetails zullen we een configuratie voor onze verbinding definiëren.

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

Voor een betere ontwikkelingservaring loggen we alle berichten met tijdsaanduidingen naar de lokale console en hebben we lage timerfrequenties ingesteld. Uw configuratie zal voor uw productieapplicatie erg verschillend zijn, dus bekijk de RxStompConfig documenten voor alle beschikbare opties.

Volgend geven we de configuratie door aan rxStomp binnen een useEffect-haken. Dit beheert de activatie van de verbinding samen met de levenscyclus van het component.

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

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

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

Hoewel er geen visuele verandering in onze app is, zou het controleren van de logs de verbinding en ping-logs moeten weergeven. Hier is een voorbeeld van hoe dat eruit zou moeten zien:

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

Note: Algemeen gezien kan het voorkomen van dubbele logboeken een teken zijn dat een deactivatie of afsluitfunctie niet correct is geïmplementeerd. React rendert elk component tweemaal in een ontwikkelingsomgeving om mensen deze fouten te helpen vangen via React.StrictMode

Hoe de Status van de Verbinding Monitoren

RxStomp heeft een RxStompState enum die de mogelijke verbindingsstatussen met onze broker weergeeft. Ons volgende doel is om de verbindingsstatus in onze UI weer te geven.

Laten we een nieuw component voor dit maken en noemen we het Status.tsx:

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

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

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

We kunnen de observabele rxStomp.connectionState$ gebruiken om vast te binden aan onze connectionStatus-string. Net zoals we useEffect hebben gebruikt, zullen we de onmount-actie gebruiken om unsubscribe() uit te voeren.

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

Om het te bekijken, includen we het in onze app:

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

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

Op dit moment zou je een werkende visuele indicator op het scherm moeten hebben. Probeer het spelletje uit door de STOMP-server down te halen en zie of de logs zoals verwacht werken.

Hoe berichten verzenden

Laten we een eenvoudige chatroom maken om een geïsoleerd eind-tot-eind berichtenverloop te laten zien met de broker.

We kunnen de functionaliteit plaatsen in een nieuw Chatroom-component. Eerst kunnen we het component maken met een aangepaste username en message-veld dat is gekoppeld aan invoervelden.

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

Laat ons dit binnen onze App plaatsen met een schakelaar om deel te nemen aan de chatroom:

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

    </>
  )

Tijd om berichten echt te verzenden. STOMP is het beste voor het verzenden van tekstgebaseerde berichten (binary data is ook mogelijk). We zullen de structuur van de data die we verzenden definiëren in een nieuw types-bestand:

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

Opmerking: Als je TypeScript niet gebruikt, kun je deze typdefinitie overslaan.

Vervolgens zullen we JSON gebruiken om het bericht te serialiseren en berichten naar onze STOMP-server verzenden met .publish met een bestemmingsonderwerp en onze JSON body.

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

Om het uit te testen, klik een paar keer op de Verzend bericht-knop en zie of de serialisatie goed werkt. Hoewel je geen visuele veranderingen zult kunnen zien, zouden de console-logs het moeten tonen:

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

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

Hoe berichten ontvangen

We zullen een nieuw component maken om de lijst met berichten van alle gebruikers te tonen. Voor nu zullen we hetzelfde type gebruiken, de naam van het onderwerp doorgeven als een prop, en alles weergeven als een lijst. Dit alles komt in een nieuw component genaamd 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>
  </>
  )
}

Tijd om alles samen te brengen!

Vergelijkbaar met het beheren van de abonnement met het Status component, stellen we het abonnement in bij het monteren en zeggen we het abonnement op bij het demonteren.

Met behulp van RxJS pipe en map, kunnen we onze JSON deserialiseren naar ons ChatMessage. Het modulaire ontwerp stelt je in staat om een ​​complexere pipeline op te zetten zoals nodig met behulp van RxJS operators.

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

  ...

Op dit punt zou de chat GUI berichten correct moeten weergeven en kun je experimenteren met het openen van meerdere tabbladen als verschillende gebruikers.

Nog iets om hier uit te proberen is het uitschakelen van de STOMP-server, het verzenden van een paar berichten en het weer inschakelen ervan. De berichten moeten lokaal in de wachtrij worden geplaatst en worden verzonden zodra de server klaar is om te gaan. Leuk!

Samenvatting

In deze tutorial hebben we:

  • @stomp/rx-stomp geïnstalleerd voor een prettige ontwikkelervaring.

  • RxStompConfig ingesteld om onze client te configureren met de verbindingsgegevens, debugger logging en timerinstellingen.

  • Gebruikte rxStomp.activate en rxStomp.deactivate om de belangrijkste levenscyclus van de client te beheren.

  • Bewakende de abonnementsstatus met behulp van rxStomp.connectionState$ observable.

  • Berichten gepubliceerd met rxStomp.publish met configureerbare bestemmingen en berichtlichamen.

  • Een observable gemaakt voor een bepaald onderwerp met behulp van rxStomp.watch.

  • Gebruikte zowel console logs als React-componenten om de bibliotheek in actie te zien en de functionaliteit en fouttolerantie te verifiëren.

Je kunt de uiteindelijke code op Gitlab vinden: https://gitlab.com/harsh183/rxstomp-react-tutorial. Voel je vrij om het ook als starttemplate te gebruiken en meld eventuele problemen die zich kunnen voordoen.