STOMP è un semplice e potente protocollo per l’invio di messaggi implementato da server popolari come RabbitMQ, ActiveMQ e Apollo. L’utilizzo di STOMP su WebSocket è un protocollo diretto, rendendolo una scelta popolare per l’invio di messaggi da un browser web, poiché protocolli come AMQP sono limitati dai browser che non consentono connessioni TCP.
Per usare STOMP su WebSocket, è possibile utilizzare @stomp/stompjs, ma questo ha callback complicati e un’API complicata che è adatta a casi d’uso specializzati. Fortunatamente, esiste anche l’ meno conosciuto @stompjs/rx-stomp che fornisce una piacevole interfaccia tramite gli osservabili di RxJS. Gli osservabili non sono esclusivi di Angular e si adattano bene a come funziona React. È una graziosa interfaccia quando si compongono flussi di lavoro e pipeline complessi con molte diverse fonti di messaggi.
Il tutorial segue più o meno il percorso simile dell’ versione iniziale in Angular, ma la struttura del componente e il stile del codice sono adattati alla stile funzionale di React.
Nota: Questo tutorial è scritto con TypeScript strict
, ma il codice JavaScript è quasi identico in quanto abbiamo solo 5 dichiarazioni di tipo. Per la versione JS, è possibile saltare le importazioni e definizioni del tipo.
Indice
Obiettivi
Qui, creerà un’applicazione di chat semplificata che mostra diversi aspetti di RxStomp in diversi componenti. In generale, vogliamo avere:
-
Un frontend React connesso con RxStomp al server STOMP.
-
Un display dello stato di connessione in tempo reale basato sulla nostra connessione al server STOMP.
-
Logica Pub/Sub per qualsiasi argomento configurabile.
-
Divisione della logica di RxStomp in componenti multipli per mostrare come separare la logica e le responsabilità.
-
Allineamento dei cicli di vita delle connessioni/iscrizioni RxStomp con i cicli di vita dei componenti React per assicurarsi che non ci siano perdite o watcher non chiusi.
Prerequisiti
-
Deve essere in esecuzione un server STOMP così che l’applicazione React possa connettersi a esso. In questo caso, userremo RabbitMQ con l’estensione
rabbitmq_web_stomp
. -
Versione React più recente. In questo tutorial userremo la versione v18, sebbene versioni più vecchie probabilmente funzionino anche bene.
-
Una certa familiarità con gli osservabili sarà anche di aiuto.
Server STOMP di partenza con RabbitMQ
Se si desidera usare anche RabbitMQ (non strettamente obbligatorio), ecco i guidi di installazione per differenti sistemi operativi. Per aggiungere l’estensione, dovrai eseguire:
$ rabbitmq-plugins enable rabbitmq_web_stomp
Se sei in grado di usare Docker
, un file Docker simile a questo imposterà tutto ciò che serve per il tutorial:
FROM rabbitmq:3.8.8-alpine
run rabbitmq-plugins enable --offline rabbitmq_web_stomp
EXPOSE 15674
Modello React di partenza
Per questo tutorial, userò il modello Vite `react-ts`. La parte centrale dell’applicazione sarà contenuta nel componente `App` e creerò componenti figlio per altre funzionalità specifiche di STOMP.
Come installare RxStomp
Utilizzerò il pacchetto npm `@stomp/rx-stomp`:
$ npm i @stomp/rx-stomp rxjs
Questo installerà la versione `2.0.0`
Nota: questo tutorial funziona anche senza specificare esplicitamente `rxjs` poiché è una dipendenza sorella, ma è una buona pratica essere espliciti a riguardo.
Come gestire la connessione e la disconnessione con il server STOMP
Adesso, apriamo `App.tsx` e inizializziamo il client `RxStomp`. Poiché il client non è uno stato che cambierà per la rendering, lo raccogliamo con il Hook `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
Prestando attenzione ai porti e ai dettagli di autenticazione predefiniti, definiremo una configurazione per la nostra connessione.
// 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() {
...
Per un migliore esperienza di sviluppo, abbiamo loggato tutti i messaggi con timestamp locali e impostato frequenze di timer basse. La vostra configurazione dovrebbe essere molto diversa per l’applicazione di produzione, quindi consultate le documetazioni di RxStompConfig per tutte le opzioni disponibili.
Successivamente, passerò la configurazione a rxStomp
all’interno di un useEffect
Hook. Questo gestisce l’attivazione della connessione insieme al ciclo di vita del componente.
// src/App.tsx
...
function App() {
const rxStompRef = useRef(new RxStomp())
const rxStomp = rxStompRef.current
useEffect(() => {
rxStomp.configure(rxStompConfig)
rxStomp.activate()
return () => {
rxStomp.deactivate()
}
})
...
Nonostante non ci sia alcuna modifica visiva nella nostra app, controllare i log dovrebbe mostrare i log di connessione e ping. Ecco un esempio di come dovrebbe apparire:
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
Nota: In generale, se vedi log duplicati, potrebbe essere un segno che una funzionalità di deattivazione o disiscrizione non è stata implementata correttamente. React renderizza ciascun componente due volte in un ambiente di sviluppo per aiutare le persone a catturare questi bug tramite React.StrictMode
Come monitorare lo stato della connessione
RxStomp ha un enum RxStompState che rappresenta gli stati di connessione possibili con il nostro broker. Il nostro prossimo obiettivo è mostrare lo stato di connessione nella nostra UI.
Creiamo un nuovo componente per questo chiamato Status.tsx
:
// src/Status.tsx
import { useState } from 'react'
export default function Status() {
const [connectionStatus, setConnectionStatus] = useState('')
return (
<>
<h2>Connection Status: {connectionStatus}</h2>
</>
)
}
Possiamo usare l’osservabile rxStomp.connectionState$
per associare la nostra stringa connectionStatus
. Come abbiamo fatto con useEffect
, userò l’azione di smontaggio per 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>
</>
)
}
Per visualizzarlo, lo includiamo nella nostra app:
// src/App.tsx
import Status from './Status'
...
return (
<>
<h1>Hello RxStomp!</h1>
<Status rxStomp={rxStomp}/>
</>
)
A questo punto, dovresti avere un indicatore visivo funzionante sullo schermo. Prova a sperimentare facendo crashare il server STOMP e vedere se i log funzionano come previsto.
Come inviare messaggi
Creiamo un semplice chatroom per mostrare un flusso di messaggistica end-to-end semplificato con il broker.
Potremmo posizionare la funzionalità in un nuovo componente Chatroom
. Prima, possiamo creare il componente con un campo personalizzato username
e message
che è legato agli input.
// 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'
/>
</>
)
}
Inclusiamo questo all’interno della nostra App con un toggle per unirsi al 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}/>
</>
)}
</>
)
Ora è il momento di inviare veramente i messaggi. STOMP è ottimo per l’invio di messaggi basati su testo (il trasferimento di dati binari è anche possibile). Definiremo la struttura dei dati che stiamo inviando in un nuovo file types:
// types.ts
interface ChatMessage {
userName: string,
message: string
}
Nota: Se non utilizzi TypeScript, puoi saltare l’aggiunta di questa definizione del tipo.
Poi, usiamo JSON per serializzare il messaggio e inviamo i messaggi al nostro server STOMP utilizzando .publish
con un topic di destinazione e il nostro 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>
</>
)
}
Per provarlo, prova a cliccare il pulsante Invia Messaggio diverse volte e vedere se la serializzazione funziona bene. Non vedrai ancora alcuna modifica visiva, ma i log della console dovrebbero mostrarlo:
Date ... >>> SEND
destination:/topic/test
content-length:45
Sent {"userName":"user722","message":"1234567890"}
Come ricevere messaggi
Creeremo un nuovo componente per mostrare l’elenco dei messaggi di tutti gli utenti. Per ora, useremo lo stesso tipo, passeremo il nome dell’argomento come prop e visualizzeremo tutto come un elenco. Tutto ciò va in un nuovo componente chiamato 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>
</>
)
}
È ora di mettere tutto insieme!
Similmente alla gestione della sottoscrizione con il componente Status
, impostiamo la sottoscrizione all’avvio e la disabilitiamo alla chiusura.
Utilizzando pipe
e map
di RxJS, possiamo deserializzare il nostro JSON nel nostro oggetto ChatMessage
. Il design modulare consente di configurare una pipeline più complessa se necessario utilizzando gli operatori di 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()
}
}, [])
...
A questo punto, l’interfaccia grafica della chat dovrebbe mostrare correttamente i messaggi e puoi sperimentare aprendo più schede come utenti diversi.
Un’altra cosa da provare qui è spegnere il server STOMP, inviare alcuni messaggi e riaccenderlo. I messaggi dovrebbero essere accodati localmente e inviati una volta che il server è pronto. Fantastico!
Riepilogo
In questo tutorial abbiamo:
-
Installato
@stomp/rx-stomp
per un’esperienza di sviluppo migliore. -
Configurato
RxStompConfig
per configurare il nostro client con i dettagli della connessione, il debug del registro e le impostazioni del timer. -
Utilizzato
rxStomp.activate
erxStomp.deactivate
per gestire il ciclo di vita principale del client. -
Monitorato lo stato della sottoscrizione usando l’osservabile
rxStomp.connectionState$
. -
Pubblicati messaggi utilizzando
rxStomp.publish
con destinazioni e corpi dei messaggi configurabili. -
Creato un osservabile per un argomento dato usando
rxStomp.watch
. -
Utilizzati sia i log della console che i componenti React per vedere la libreria in azione e verificare la funzionalità e la tolleranza ai guasti.
Puoi trovare il codice finale su Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Sentiti libero di usarlo anche come modello iniziale e segnala eventuali problemi che potrebbero sorgere.
Source:
https://www.freecodecamp.org/news/build-chat-app-with-stomp-and-react/