STOMP – это удивительно простая, но мощная протокол для отправки сообщений, реализованный популярными серверами, такими как RabbitMQ, ActiveMQ и Apollo. Utilizando STOMP через WebSocket, вы используете простой протокол, что делает его популярным выбором для отправки сообщений из веб-браузера, поскольку протоколы, такие как AMQP, ограничены браузерами, которые не допускают TCP-соединения.

Для использования STOMP через WebSocket вы можете использовать @stomp/stompjs, но он имеет сложные вызовыallback и сложную API, которая соответствует специфическим сценариям использования. К счастью, также существует менее известный @stompjs/rx-stomp, который предоставляет приятный интерфейс через наблюдаемые RxJS. Observables не специфичны для Angular, и они хорошо соответствуют тому, как работает React. Это замечательный интерфейс, когда вы составляете сложные рабочие процессы и потоки с множеством различных источников сообщений.

Руководство следует схожему пути, как и первая версия в Angular, но структура компонентов и стиль кода направлены на функциональный стиль React.

Примечание: Это руководство написано с использованием strict TypeScript, но JavaScript-код практически идентичен, поскольку у нас только 5 деклараций типов. Чтобы JS-версия, вы можете пропустить импорты и определения типов.

Contents

Цели

В этом разделе мы создадим упрощенное приложение чата, которое показывает различные аспекты RxStomp в различных компонентах. Общая цель состоит в том, чтобы иметь:

  • React-формунд, соединенный с RxStomp и STOMP сервером.

  • Действующее отображение состояния соединения на основе нашего соединения с STOMP сервером.

  • Логика публикации/подписки на любой конфигурируемый топик.

  • Разделение логики RxStomp между множеством компонентов для показа того, как разделить логику и ответственность.

  • Согласование жизненных циклов подключения/подписки RxStomp с жизненными циклами компонентов React, чтобы избежать утечек или незакрытых наблюдателей.

Предварительные требования

  • У вас должен быть запущен сервер STOMP, чтобы приложение React могло к нему подключиться. Здесь мы будем использовать RabbitMQ с расширением rabbitmq_web_stomp.

  • Последняя версия React. В этом учебнике будет использоваться версия v18, хотя, вероятно, подойдут и более старые версии.

  • Некоторое знакомство с observables также будет полезным.

Начальный сервер STOMP с RabbitMQ

Если вы тоже хотите использовать RabbitMQ (это не строго обязательно), вот руководства по установке для разных операционных систем. Чтобы добавить расширение, вам нужно будет запустить:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Если вы можете использовать Docker, Docker-файл, похожий на этот, настроит все необходимое для учебника:

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

Демонстрационный React шаблон

Для этого учебника мы будем использовать шаблон Vite react-ts. Сердцем нашего приложения будет компонент App, и мы создадим дочерние компоненты для других специфических функций STOMP.

Как установить RxStomp

Мы будем использовать npm пакет @stomp/rx-stomp:

$ npm i @stomp/rx-stomp rxjs

Это установит версию 2.0.0

Обратите внимание: Этот учебник работает и без явного указания rxjs, так как он является сестрой зависимости, но хорошей практикой является явное указание.

Как управлять соединением и отключением с сервером STOMP

Теперь откройте App.tsx и инициализируйте наш клиент RxStomp. Так как клиент не является состоянием, которое изменится для отображения, мы заключим его в кук 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

Предполагая, что используются стандартные порты и данные аутентификации, мы определим несколько настроек для нашей связи.

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

Чтобы улучшитьDev опыт, мы вывели все сообщения с временем戳 в локальный консоль и установили низкие частоты таймера. Ваша конфигурация для вашего производственного приложения должна быть совершенно другая, так что посмотрите на документацию RxStompConfig для всех доступных настроек.

Далее мы передадим конфигурацию в rxStomp внутри useEffect Hook. Это управляет активацией соединения вместе с циклом жизни компонента.

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

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

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

尽管在我们的应用程序中没有视觉变化,但在检查日志时应该会显示连接和心跳日志。以下是一个例子,展示了这应该是什么样子:

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

注意:通常,如果你看到重复的日志,这可能是未正确实现去激活或取消订阅功能的迹象。在开发环境中,React会渲染每个组件两次以帮助人们通过React.StrictMode捕获这些错误

如何监控连接状态

RxStomp有一个RxStompState枚举,表示与我们的代理可能存在的连接状态。我们的下一个目标是将连接状态显示在UI中。

让我们为此创建一个新组件,称为Status.tsx

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

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

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

我们可以使用rxStomp.connectionState$可观察对象将我们的connectionStatus字符串绑定。与使用useEffect相似,我们将使用卸载操作来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>
    </>
  )
}

为了查看它,我们在应用程序中包含它:

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

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

На этой стадии у вас должен быть работающий визуальный индикатор на экране. Попробуйте играться, отключив STOMP-сервер и увидеть, работают ли логи как ожидалось.

Как отправлять сообщения

Давайте создадим простой чат-комнату, чтобы продемонстрировать упрощенный полный цикл отправки сообщений с помощью брокера.

Мы можем разместить функциональность в новом компоненте Chatroom. Сначала мы можем создать компонент с пользовательским полем username и message, связанным с входными данными.

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

Подключим это в наш App с переключателем для присоединения к чат-комнате:

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

    </>
  )

Время отправлять сообщения на самом деле. STOMP лучший для отправки текстовых сообщений (binрные данные также возможны). Мы будем определять структуру данных, которую мы отправляем, в новом файле types:

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

Примечание: Если вы не используете TypeScript, можете пропустить добавление этого определения типа.

Далее мы используем JSON для сериализации сообщения и отправляем сообщения на наш STOMP-сервер с использованием .publish с топиком назначения и нашей 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>
    </>
  )
}

Для проверки попробуйте нажать кнопку Отправить сообщение несколько раз и увидеть, работает ли сериализация хорошо.尽管在视觉上还看不出任何变化,控制台日志应该会显示它:

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

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

Как получать сообщения

Мы создадим новый компонент для отображения списка сообщений от всех пользователей. Пока мы будем использовать тот же тип, передавать название темы в качестве пропса и отображать все в виде списка. Все это будет помещено в новый компонент под названием 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>
  </>
  )
}

Пришло время собрать все вместе!

Аналогично управлению подпиской в компоненте Status, мы настраиваем подписку при монтировании и отписываемся при размонтировании.

Используя pipe и map из RxJS, мы можем десериализовать наш JSON обратно в ChatMessage. Модульный дизайн позволяет при необходимости настроить более сложный конвейер, используя операторы 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()
    }
  }, [])

  ...

На этом этапе интерфейс чата должен корректно отображать сообщения, и вы можете поэкспериментировать, открыв несколько вкладок от имени разных пользователей.

Еще один интересный эксперимент – выключить STOMP-сервер, отправить несколько сообщений и снова включить его. Сообщения должны локально поставиться в очередь и отправиться, как только сервер будет готов. Здорово!

Итоги

В этом уроке мы:

  • Установили @stomp/rx-stomp для удобной разработки.

  • Настроили RxStompConfig для конфигурации нашего клиента с параметрами подключения, логированием отладки и настройками таймера.

  • Используйте rxStomp.activate и rxStomp.deactivate для управления основным жизненным циклом клиента.

  • Отслеживайте состояние подписки с помощью observable rxStomp.connectionState$.

  • Публикуйте сообщения с помощью rxStomp.publish с настраиваемыми пунктами назначения и текстом сообщений.

  • Создайте observable для заданной темы с помощью rxStomp.watch.

  • Используйте как журналы консоли, так и компоненты React, чтобы увидеть библиотеку в действии и проверить функциональность и отказоустойчивость.

Вы можете найти окончательный код на Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. Не стесняйтесь использовать его в качестве стартового шаблона и сообщать о любых возникающих проблемах.