STOMP est un protocole extrêmement simple et puissant pour l’envoi de messages mis en œuvre par des serveurs populaires tels que RabbitMQ, ActiveMQ et Apollo. L’utilisation de STOMP sur WebSocket est un protocole direct et facile à utiliser, ce qui en fait une solution populaire pour l’envoi de messages depuis un navigateur Web, car des protocoles tels que AMQP sont limités par les navigateurs qui ne permettent pas les connexions TCP.

Pour utiliser STOMP sur WebSocket, vous pouvez utiliser @stomp/stompjs, mais il possède des callbacks très subtilement impliqués et une API compliquée conçue pour des cas d’utilisation plus spécialisés. Heureusement, il existe également le moins connu @stompjs/rx-stomp qui offre une interface agréable grâce à des observables de RxJS. Les observables ne sont pas exclusifs à Angular et s’intègrent assez bien à la manière dont React fonctionne. C’est une interface sympa lorsque vous composez des flux de travail et des pipelines complexes avec de nombreuses sources de messages différentes.

Le tutoriel suit une voie relativement similaire à la version initiale dans Angular, mais la structure du composant et le style du code sont axés sur le style fonctionnel de React.

Note: Ce tutoriel est écrit en TypeScript strict, mais le code JavaScript est presque identique étant donné que nous n’avons que 5 déclarations de type. Pour la version JS, vous pouvez ignorer les importations et définitions de types.

Table des matières

Objectifs

Ici, nous construirons une application de salon de discussion simplifiée qui montre différents aspects de RxStomp dans différents composants. Globalement, nous voulons avoir :

  • Un frontend React connecté à RxStomp vers un serveur STOMP.

  • Un affichage en direct de l’état de connexion basé sur notre connexion au serveur STOMP.

  • Une logique Pub/Sub pour n’importe quel sujet configurable.

  • Séparer la logique RxStomp sur plusieurs composants pour montrer comment séparer la logique et la responsabilité.

  • Aligner les cycles de connexion/abonnement de RxStomp avec les cycles des composants React pour s’assurer qu’il n’y a pas de fuites ou de watchers non fermés.

Préalables

  • Vous devez avoir un serveur STOMP en cours d’execution afin que l’application React puisse se connecter à celui-ci. Ici, nous utiliserons RabbitMQ avec l’extension rabbitmq_web_stomp.

  • Dernière version de React. Cet tutoriel utilisera la version 18, bien que les versions plus anciennes fonctionnent probablement également.

  • Une certaine familiarité avec les observables sera également utile.

Serveur STOMP de départ avec RabbitMQ

Si vous souhaitez également utiliser RabbitMQ (non strictement obligatoire), voici les guides d’installation pour différents systèmes d’exploitation. Pour ajouter l’extension, vous devez exécuter :

$ rabbitmq-plugins enable rabbitmq_web_stomp

Si vous pouvez utiliser Docker, un fichier Docker similaire à ceci configure tout ce qu’il faut pour ce tutoriel :

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Modèle de démarrage React

Pour ce tutoriel, nous utiliserons le modèle Vite de react-ts. La partie centrale de notre application se trouvera dans le composant App, et nous créerez des composants enfants pour d’autres fonctionnalités spécifiques de STOMP.

Comment installer RxStomp

Nous utiliserons le package npm @stomp/rx-stomp :

$ npm i @stomp/rx-stomp rxjs

Cela installera la version 2.0.0

Note : Ce tutoriel fonctionne encore sans spécifier explicitement rxjs car c’est une dépendance sœur, mais il est recommandé de le faire explicitement.

Comment gérer la connexion et la déconnexion avec le serveur STOMP

Maintenant, ouvrons App.tsx et initialisons notre client RxStomp. Comme le client n’est pas un état qui changera pour le rendu, nous l’enveloppons dans l’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

En supposant les ports et détails d’authentification par défaut, nous définirons ensuite une configuration pour notre connexion.

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

Pour un meilleur expérience de développement, nous avons enregistré toutes les messages avec des horodatages locaux dans la console et mis les fréquences des déclencheurs basses. Votre configuration pourra être très différente pour votre application de production, donc consultez les documents de RxStompConfig pour toutes les options disponibles.

Next, we’ll pass the configuration to rxStomp inside a useEffect Hook. This manages the connection’s activation alongside the component lifecycle.

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

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

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

While there’s no visual change in our app, checking the logs should show connection and ping logs. Here’s an example of what that should look like:

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: Generally, if you see duplicate logs, it may be a sign that a deactivation or unsubscribe functionality wasn’t implemented correctly. React renders each component twice in a dev environment to help people catch these bugs via React.StrictMode

How to Monitor the Connection Status

RxStomp has a RxStompState enum that represents possible connection states with our broker. Our next goal is to display the connection status in our UI.

Let’s create a new component for this called Status.tsx:

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

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

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

We can use the rxStomp.connectionState$ observable to bind to our connectionStatus string. Similar to how we used useEffect, we’ll use the unmount action to 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>
    </>
  )
}

To view it, we include it in our app:

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

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

A ce stade, vous devriez avoir un indicateur visuel fonctionnel sur l’écran. Essayez de jouer avec la désactivation du serveur STOMP et verifiez si les journaux fonctionnent comme prévu.

Comment Envoyer des Messages

Créons un simple salon de discussion pour montrer un flux de messages simplifié de bout en bout avec le broker.

Nous pouvons placer la fonctionnalité dans un nouveau composant Chatroom. Premièrement, nous pouvons créer le composant avec un champ personnalisé username et message lié aux entrées.

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

Inclutons cela dans notre App avec un bouton pour rejoindre le salon de discussion :

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

    </>
  )

Il est temps d’envoyer des messages réellement. STOMP est idéal pour l’envoi de messages texte (le transfert de données binaires est également possible). Nous définirons la structure des données que nous envoyons dans un nouveau fichier types :

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

Note: Si vous n’utilisez pas TypeScript, vous pouvez sauter l’ajout de cette définition de type.

Ensuite, utilisons JSON pour sérialiser le message et envoyons des messages à notre serveur STOMP en utilisant .publish avec un sujet de destination et notre corps 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>
    </>
  )
}

Pour l’essayer, essayez de cliquer sur le bouton Envoyer un Message quelques fois et vérifiez si la sérialisation fonctionne bien. Malgré le fait que vous ne verrez pas d’évolutions visuelles pour l’instant, les journaux de console devraient le montrer :

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

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

Comment Recevoir des Messages

Nous créerons un nouveau composant pour afficher la liste des messages de tous les utilisateurs. Pour l’instant, nous utiliserons le même type, transmettons le nom du sujet en tant que prop et affichons tout comme une liste. Tout cela se place dans un nouveau composant appelé 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>
  </>
  )
}

C’est le moment de rassembler tout !

Pareil que pour gérer l’abonnement avec le composant Status, nous établissons l’abonnement au montage et désabonnons à la désinstallation.

En utilisant RxJS pipe et map, nous pouvons désérialiser notre JSON en notre ChatMessage. Le design modulaire permet de configurer une pipeline plus compliquée en utilisant les opérateurs 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 ce stade, la JUI du chat devrait afficher les messages correctement, et vous pouvez expérimenter avec l’ouverture de plusieurs onglets en tant que différents utilisateurs.

Une autre chose à essayer ici est d’éteindre le serveur STOMP, d’envoyer quelques messages, et de le rallumer. Les messages devraient être mis en file d’attente localement et être envoyés une fois que le serveur est prêt. Sympa !

Résumé

Dans ce tutoriel, nous avons :

  • Installé @stomp/rx-stomp pour une belle expérience de développement.

  • Configuré RxStompConfig pour configurer notre client avec les détails de connexion, le logging du débuggeur et les réglages de délai.

  • Utiliser rxStomp.activate et rxStomp.deactivate pour gérer le cycle de vie principal du client.

  • Surveiller l’état de l’abonnement à l’aide de l’observable rxStomp.connectionState$.

  • Publier des messages à l’aide de rxStomp.publish avec des destinations et des corps de messages configurables.

  • Créer un observable pour un sujet donné à l’aide de rxStomp.watch.

  • Utiliser à la fois les journaux de console et les composants React pour observer la bibliothèque en action, et vérifier la fonctionnalité et la tolérance aux panneaux.

Vous pouvez trouver le code final sur Gitlab : https://gitlab.com/harsh183/rxstomp-react-tutorial. N’hésitez pas à l’utiliser comme modèle de départ également et rapportez tous les problèmes qui peuvent survenir.