STOMP é um protocolo simples e poderoso para enviar mensagens, implementado por servidores populares como RabbitMQ, ActiveMQ e Apollo. Ao usar STOMP sobre WebSocket, um protocolo direto, torna-se uma escolha popular para enviar mensagens a partir de um navegador da Web, pois protocolos como AMQP são limitados por browsers que não permitem conexões TCP.

Para usar STOMP sobre WebSocket, você pode usar @stomp/stompjs, mas esse possui callbacks complicados e uma API complexa que atende a casos de uso mais especializados. Felizmente, também existe o menos conhecido @stompjs/rx-stomp que fornece uma interface agradável através de observáveis de RxJS. Observáveis não são exclusivos do Angular e se encaixam bem com a maneira como o React funciona. É uma interface interessante quando compondo fluxos de trabalho e pipeline complexos com muitas fontes de mensagens diferentes.

O tutorial segue um caminho semelhante à versão inicial em Angular, mas a estrutura de componente e o estilo de código são ajustados para o estilo funcional do React.

Nota: Este tutorial está escrito com TypeScript strict, mas o código em JavaScript é quase idêntico, já que temos apenas 5 declarações de tipo. Para a versão JS, você pode pular as importações e definições de tipo.

Sumário

Metais

Aqui, vamos construir um aplicativo de chat simples que mostra vários aspectos de RxStomp em diferentes componentes. Globalmente, queremos ter:

  • Um frontend React conectado com RxStomp a um servidor STOMP.

  • Uma exibição de status de conexão em tempo real baseada na nossa conexão com o servidor STOMP.

  • Logica Pub/Sub para qualquer tópico configurável.

  • Separação da lógica de RxStomp em componentes diferentes para mostrar como separar logica e responsabilidade.

  • Alinhando os ciclos de vida de conexão/assinatura do RxStomp com os ciclos de vida dos componentes React para garantir que não haja vazamentos ou observadores não fechados.

Pré-requisitos

  • Você deve ter um servidor STOMP em funcionamento para que a aplicação React possa se conectar a ele. Aqui, usaremos o RabbitMQ com a extensão rabbitmq_web_stomp.

  • Última versão do React. Este tutorial usará a versão 18, embora versões mais antigas provavelmente também funcionem.

  • Alguma familiaridade com observáveis também ajudará.

Servidor STOMP inicial com RabbitMQ

Se você também quiser usar o RabbitMQ (não é estritamente necessário), aqui estão guias de instalação para diferentes sistemas operacionais. Para adicionar a extensão, você precisará executar:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Se você puder usar Docker, um arquivo Docker semelhante a este configurará tudo o que é necessário para o tutorial:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Modelo de Início React

Para este tutorial, usaremos o modelo `react-ts` do Vite. O componente central de nossa aplicação estará no componente App e criaremos componentes filhos para outras funcionalidades específicas de STOMP.

Como Instalar o RxStomp

Usaremos o pacote npm @stomp/rx-stomp:

$ npm i @stomp/rx-stomp rxjs

Isto instalará a versão 2.0.0

Nota: Este tutorial ainda funciona sem especificar explicitamente rxjs pois é uma dependência irmã, mas é uma boa prática ser explícito quanto a isso.

Como Gerenciar Conexão e Desconexão com o Servidor STOMP

Agora, abra App.tsx e inicialize o cliente RxStomp. since the client isn’t a state that will change for rendering, vamos envolver-o no gancho de 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

Assumindo os portos padrão e detalhes de autenticação, vamos definir alguma configuração para nossa conexão a seguir.

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

Para uma melhor experiência de desenvolvimento, registramos todas as mensagens com marcadores de tempo no console local e definimos freqüências de temporizador baixas. Sua configuração deve ser bem diferente para sua aplicação de produção, então confira os documentos de RxStompConfig para todas as opções disponíveis.

Agora, vamos passar a configuração para o rxStomp dentro de um useEffect Hook. Isso gerencia a ativação da conexão juntamente com o ciclo de vida do componente.

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

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

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

Enquanto não há mudança visual em nosso app, a verificação dos logs deve mostrar logs de conexão e ping. Aqui está um exemplo de como isso deve parecer:

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: Normalmente, se você ver logs duplicados, pode ser um sinal de que uma funcionalidade de desativação ou desinscrição não foi implementada corretamente. O React renderiza cada componente duas vezes em um ambiente de desenvolvimento para ajudar as pessoas a pegar esses bugs por meio de React.StrictMode

Como Monitorizar o Status da Conexão

O RxStomp tem um enum RxStompState que representa os estados de conexão possíveis com o nosso broker. O próximo objetivo é exibir o status de conexão na nossa UI.

Vamos criar um novo componente para isso chamado Status.tsx:

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

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

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

Podemos usar o observável rxStomp.connectionState$ para associar à nossa string connectionStatus. Similar a como usamos useEffect, vamos usar a ação de desmontagem para 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>
    </>
  )
}

Para visualizá-lo, incluímos-o em nossa app:

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

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

Neste ponto, você deveria ter um indicador visual funcional na tela. Tente brincar com a desconexão do servidor STOMP e ver se os logs funcionam como esperado.

Como Enviar Mensagens

Vamos criar um simples chatroom para mostrar um fluxo de mensagem simples de ponta a ponta com o broker.

Podemos colocar a funcionalidade em um novo componente Chatroom. Primeiro, podemos criar o componente com um campo personalizado de username e message que está associado com entradas.

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

Vamos incluir isso dentro de nossa App com um toggle para entrar no 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}/>
        </>
      )}

    </>
  )

É hora de enviar mensagens realmente. STOMP é o melhor para enviar mensagens baseadas em texto (dados binários também são possíveis). Vamos definir a estrutura dos dados que estamos enviando the um novo arquivo types:

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

Nota: Se você não estiver usando TypeScript, você pode pular a adição desta definição de tipo.

A seguir, vamos usar JSON para serializar a mensagem e enviar mensagens para o nosso servidor STOMP usando .publish com um tópico de destino e o nosso 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>
    </>
  )
}

Para testá-lo, tente clicar no botão Enviar Mensagem algumas vezes e ver se a serialização funciona bem. Ainda não será possível ver mudanças visuais ainda, mas os logs do console deveriam mostrar isso:

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

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

Como Receber Mensagens

Vamos criar um novo componente para mostrar a lista de mensagens de todos os usuários. Por enquanto, usaremos o mesmo tipo, passaremos o nome do tópico como uma propriedade e exibiremos tudo como uma lista. Tudo isso vai em um novo componente chamado 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>
  </>
  )
}

Hora de juntar tudo!

Semelhante à gestão da assinatura com o componente Status, configuramos a assinatura na montagem e cancelamos a assinatura na desmontagem.

Usando RxJS pipe e map, podemos desserializar nosso JSON de volta para nossa ChatMessage. O design modular permite que você configure um pipeline mais complicado conforme necessário usando operadores do 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()
    }
  }, [])

  ...

Neste ponto, a GUI do chat deve mostrar as mensagens corretamente, e você pode experimentar abrir várias abas como usuários diferentes.

Outra coisa a tentar aqui é desligar o servidor STOMP, enviar algumas mensagens e ligá-lo novamente. As mensagens devem ser enfileiradas localmente e enviadas assim que o servidor estiver pronto para funcionar. Legal!

Resumo

Neste tutorial, nós:

  • Instalamos @stomp/rx-stomp para uma boa experiência de desenvolvimento.

  • Configuramos RxStompConfig para configurar nosso cliente com os detalhes de conexão, registro de depuração e configurações de temporizador.

  • Usou rxStomp.activate e rxStomp.deactivate para gerenciar o ciclo de vida principal do cliente.

  • Monitorou o estado da assinatura usando o observável rxStomp.connectionState$.

  • Publicou mensagens usando rxStomp.publish com destinos e corpos de mensagem configuráveis.

  • Criou um observável para um determinado tópico usando rxStomp.watch.

  • Usou tanto logs no console quanto componentes de React para ver a biblioteca em ação, e verificar funcionalidade e resistência a falhas.

Você pode encontrar o código final no Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Fique livre para usá-lo como um modelo inicial também e relatar quaisquer problemas que possam surgir.