STOMP é um protocolo simples e poderoso para envio de mensagens implementado por servidores populares como RabbitMQ, ActiveMQ e Apollo. Ao usar STOMP sobre WebSocket, é um protocolo direto, tornando-se uma escolha popular para o envio de mensagens a partir de um navegador da Internet, porque 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 isso tem 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 via observáveis de RxJS. Observáveis não são exclusivos de Angular e funcionam bem com o modo de funcionamento do React. É uma interface interessante quando compondo fluxos de trabalho e pipeline complexos com muitas fontes de mensagem diferentes.
O tutorial segue um caminho semelhante à versão inicial em Angular, mas a estrutura do componente e o estilo do código são ajustados para o estilo funcional do React.
Nota: Este tutorial está escrito com TypeScript strict
, mas o código 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 simplificado que mostra aspectos diferentes de RxStomp em componentes diferentes. 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.
-
Dividindo a lógica de RxStomp entre componentes diferentes para mostrar como separar logica e responsabilidade.
-
Alinhando os ciclos de vida das conexões/assinaturas RxStomp com os ciclos de vida dos componentes React para garantir que não haja leaks ou watchers não fechados.
Pré-requisitos
-
Você deve ter um servidor STOMP rodando para que a aplicação React possa se conectar a ele. Aqui, usaremos RabbitMQ com a extensão
rabbitmq_web_stomp
. -
Versão mais recente do React. Este tutorial usará a versão v18, embora versões mais antigas provavelmente funcionem bem também.
-
Alguma familiaridade com observáveis também será de ajuda.
Servidor STOMP iniciante com RabbitMQ
Se você quiser usar RabbitMQ também (não obrigató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ê for capaz de usar Docker
, um arquivo Docker semelhante a isto 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 Iniciante de 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
, já que é uma dependência irmã, mas é uma boa prática ser explícito sobre 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, we’ll wrap it in the useRef
Hook.
// 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, definiremos algumas configurações 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 timestamps para um console local e definimos frequências de timer baixas. Sua configuração deve ser bem diferente para sua aplicação de produção, portanto, 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 do que esse aspecto deve se 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 função 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 apanhar esses bugs via React.StrictMode
Como monitorar o status da conexão
O RxStomp tem um enum RxStompState que representa os estados de conexão possíveis com 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>
</>
)
}
Nós podemos usar o observável rxStomp.connectionState$
para associar com nossa string connectionStatus
. Semelhante ao modo 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 o mesmo, incluímo-lo 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.
Nós podemos colocar a funcionalidade em um novo componente Chatroom
. Primeiro, podemos criar o componente com um campo personalizado 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 interruptor 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. O 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 está usando TypeScript, pode pular a adição desta definição de tipo.
A próxima coisa, vamos usar JSON para serializar a mensagem e enviar mensagens para o nosso servidor STOMP usando .publish
com um tópico de destino e nosso corpo 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 verifique se a serialização funciona bem. Embora você não considere que haja mudanças visuais ainda, as mensagens do console deveriam mostrar:
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, vamos usar o mesmo tipo, passar o nome do tópico como uma propriedade, e exibir tudo como uma lista. Tudo isso vai para 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!
Similar a gerenciar a assinatura com o componente Status
, nós configuramos a assinatura ao montar, e desinscrevemos ao desmontar.
Usando o RxJS pipe
e map
, nós podemos desserializar o nosso JSON de volta para o nosso ChatMessage
. O design modular permite que você configure uma pipeline mais complicada 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 interface do chat deve mostrar as mensagens corretamente, e você pode experimentar com abrir múltiplas abas como usuários diferentes.
Outra coisa para tentar aqui é desligar o servidor STOMP, enviar algumas mensagens, e ligá-lo de volta. As mensagens devem ficar em fila localmente e ser enviadas uma vez que o servidor esteja pronto. Legal!
Resumo
Neste tutorial, nós:
-
Instalamos
@stomp/rx-stomp
para uma experiência de desenvolvimento agradável. -
Configuramos
RxStompConfig
para configurar o nosso cliente com os detalhes de conexão, log de depuração e configurações de timer. -
Usado
rxStomp.activate
erxStomp.deactivate
para gerenciar o ciclo de vida principal do cliente. -
Monitorar o estado da assinatura usando o observável
rxStomp.connectionState$
. -
Publicar mensagens usando
rxStomp.publish
com destinos e corpos de mensagem configuráveis. -
Criar um observável para um determinado tópico usando
rxStomp.watch
. -
Usar logs de console e componentes React para ver a biblioteca em ação e verificar a funcionalidade e tolerância a falhas.
Você pode encontrar o código final no Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Sinta-se à vontade para usá-lo como um modelo inicial e relatar quaisquer problemas que possam surgir.
Source:
https://www.freecodecamp.org/news/build-chat-app-with-stomp-and-react/