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
erxStomp.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.
Source:
https://www.freecodecamp.org/news/build-chat-app-with-stomp-and-react/