Библиотека STOMP.js применяется для построения систем обмена сообщениями поверх WebSocket. Наиболее распространённый сценарий — реализация чатов, уведомлений, систем поддержки, совместного редактирования документов и панелей мониторинга.
В основе лежит протокол STOMP (Simple Text Oriented Messaging Protocol), который предоставляет поверх WebSocket полноценную модель обмена сообщениями:
Типичная схема работы реального чата выглядит следующим образом:
Браузер
↓
STOMP.js
↓
WebSocket
↓
STOMP Broker
↓
Другие клиенты
В роли брокера могут выступать:
Современная версия STOMP.js распространяется через пакет
@stomp/stompjs.
Установка через npm:
npm install @stomp/stompjs
Подключение:
import { Client } from '@stomp/stompjs';
При использовании SockJS:
npm install sockjs-client
import SockJS from 'sockjs-client';
Минимальная конфигурация клиента:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
reconnectDelay: 5000
});
client.activate();
Параметр brokerURL содержит адрес WebSocket
endpoint.
Параметр reconnectDelay активирует автоматическое
переподключение при потере соединения.
Некоторые серверы не поддерживают чистый WebSocket или работают за прокси, ограничивающими апгрейд соединения. В таких случаях применяется SockJS.
Пример:
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
const client = new Client({
webSocketFactory: () => new SockJS('http://localhost:8080/chat'),
reconnectDelay: 5000
});
client.activate();
webSocketFactory вызывается при каждом
переподключении.
Чаты почти всегда требуют идентификации клиента.
STOMP поддерживает передачу заголовков подключения:
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
connectHeaders: {
login: 'user',
passcode: 'password',
Authorization: 'Bearer JWT_TOKEN'
}
});
На сервере заголовки могут использоваться для:
После установки соединения вызывается onConnect.
Именно здесь обычно выполняются:
Пример:
client.onConn ect = () => {
console.log('Подключение установлено');
};
Чат строится вокруг подписок.
Пример подписки на общий канал:
client.onConn ect = () => {
client.subscribe('/topic/chat', message => {
console.log(message.body);
});
};
message.body содержит строковое тело сообщения.
Чаще всего используется JSON.
Отправка:
client.publish({
destination: '/app/chat',
body: JSON.stringify({
author: 'Alex',
text: 'Привет',
time: Date.now()
})
});
Получение:
client.subscribe('/topic/chat', message => {
const data = JSON.parse(message.body);
console.log(data.author);
console.log(data.text);
});
Практически полезная структура:
{
id: 'msg-101',
roomId: 'general',
author: 'Alex',
text: 'Сообщение',
createdAt: 1710000000,
edited: false,
attachments: [],
type: 'MESSAGE'
}
Дополнительные типы сообщений:
{
type: 'JOIN'
}
{
type: 'LEAVE'
}
{
type: 'TYPING'
}
{
type: 'READ'
}
Для разделения сообщений применяются отдельные топики.
Подписка:
client.subscribe('/topic/room/general', callback);
Отправка:
client.publish({
destination: '/app/room/general',
body: JSON.stringify(message)
});
Динамическое переключение комнат:
let currentSubscription = null;
function joinRoom(roomId) {
if (currentSubscription) {
currentSubscription.unsubscribe();
}
currentSubscription = client.subscribe(
`/topic/room/${roomId}`,
message => {
console.log(message.body);
}
);
}
Приватные сообщения обычно маршрутизируются через персональные очереди.
Подписка:
client.subscribe('/user/queue/private', message => {
const data = JSON.parse(message.body);
console.log(data);
});
Отправка:
client.publish({
destination: '/app/private',
body: JSON.stringify({
to: 'user2',
text: 'Приватное сообщение'
})
});
STOMP не хранит сообщения самостоятельно. История должна загружаться отдельно через HTTP API.
Типичный сценарий:
async function loadHistory(roomId) {
const response = await fetch(`/api/rooms/${roomId}/messages`);
return await response.json();
}
После загрузки истории подключается realtime-канал.
При переподключениях возможно повторное получение сообщений.
Поэтому сообщениям назначаются уникальные идентификаторы.
Пример фильтрации:
const receivedMessages = new Set();
client.subscribe('/topic/chat', message => {
const data = JSON.parse(message.body);
if (receivedMessages.has(data.id)) {
return;
}
receivedMessages.add(data.id);
renderMessage(data);
});
STOMP поддерживает ACK/NACK.
Подписка:
client.subscribe(
'/topic/chat',
message => {
try {
const data = JSON.parse(message.body);
processMessage(data);
message.ack();
} catch (e) {
message.nack();
}
},
{
ack: 'client'
}
);
Режимы подтверждения:
autoclientclient-individualЧастая функция современных чатов.
Отправка события:
input.addEventListener('input', () => {
client.publish({
destination: '/app/typing',
body: JSON.stringify({
roomId: 'general',
user: 'Alex'
})
});
});
Получение:
client.subscribe('/topic/typing', message => {
const data = JSON.parse(message.body);
showTyping(data.user);
});
Presence показывает:
Сообщение подключения:
client.publish({
destination: '/app/presence',
body: JSON.stringify({
status: 'ONLINE'
})
});
Подписка:
client.subscribe('/topic/presence', message => {
const data = JSON.parse(message.body);
updateUserStatus(data);
});
Heartbeat предотвращает “зависшие” соединения.
Настройка:
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
Параметры указываются в миллисекундах.
При потере сети STOMP.js способен автоматически восстанавливать соединение.
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
reconnectDelay: 5000
});
Интервал переподключения:
5000 ms = 5 секунд
Ошибка STOMP-протокола:
client.onStompEr ror = frame => {
console.error(frame.headers.message);
console.error(frame.body);
};
Ошибка WebSocket:
client.onWebSocketEr ror = error => {
console.error(error);
};
Закрытие соединения:
client.onWebSocketCl ose = () => {
console.log('Соединение закрыто');
};
При большом количестве пользователей возникают проблемы:
Типичная архитектура:
Browser
↓
Load Balancer
↓
WebSocket Cluster
↓
Message Broker
↓
Database
Часто применяются:
RabbitMQ поддерживает STOMP через отдельный плагин.
Подключение:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
connectHeaders: {
login: 'guest',
passcode: 'guest'
}
});
Подписка:
client.subscribe('/queue/chat', message => {
console.log(message.body);
});
Вместо частой отправки маленьких сообщений:
[
{ text: '1' },
{ text: '2' },
{ text: '3' }
]
Плохой подход:
messages.forEach(renderMessage);
Лучший вариант:
const fragment = document.createDocumentFragment();
messages.forEach(message => {
fragment.append(createMessageElement(message));
});
container.append(fragment);
Чат не должен бесконечно накапливать DOM-элементы.
Пример:
const MAX_MESSAGES = 100;
function trimMessages() {
while (container.children.length > MAX_MESSAGES) {
container.removeChild(container.firstChild);
}
}
Клиентская защита:
let lastMessageTime = 0;
function canSendMessage() {
const now = Date.now();
if (now - lastMessageTime < 1000) {
return false;
}
lastMessageTime = now;
return true;
}
Серверная защита обычно включает:
Иногда необходимо хранить локальную очередь.
const pendingMessages = [];
function sendMessage(message) {
pendingMessages.push(message);
client.publish({
destination: '/app/chat',
body: JSON.stringify(message)
});
}
После подтверждения сообщение удаляется из очереди.
STOMP ориентирован на текстовые данные, поэтому файлы обычно передаются отдельно через HTTP.
Схема:
1. Upload файла
2. Получение URL
3. Отправка URL через STOMP
Сообщение:
{
text: 'Файл',
attachment: {
url: '/uploads/file.pdf',
name: 'file.pdf'
}
}
Для production-среды используется только WSS.
const client = new Client({
brokerURL: 'wss://example.com/chat'
});
Дополнительно применяются:
При уничтожении интерфейса необходимо закрывать подписки.
const subscription = client.subscribe(
'/topic/chat',
callback
);
subscription.unsubscribe();
Полное отключение:
client.deactivate();
Базовый пример:
import { useEffect } from 'react';
import { Client } from '@stomp/stompjs';
export default function Chat() {
useEffect(() => {
const client = new Client({
brokerURL: 'ws://localhost:8080/chat'
});
client.onConn ect = () => {
client.subscribe('/topic/chat', message => {
console.log(message.body);
});
};
client.activate();
return () => {
client.deactivate();
};
}, []);
}
Пример:
import { Client } from '@stomp/stompjs';
export default {
mounted() {
this.client = new Client({
brokerURL: 'ws://localhost:8080/chat'
});
this.client.activate();
},
beforeUnmount() {
this.client.deactivate();
}
}
В Bitrix STOMP.js часто используется для:
Пример подключения внутри Bitrix-компонента:
BX.ready(() => {
const client = new StompJs.Client({
brokerURL: 'ws://localhost:8080/chat'
});
client.activate();
});
Встроенный debug-режим:
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
debug: str => {
console.log(str);
}
});
Логи помогают анализировать:
Мобильные браузеры ограничивают таймеры и сетевую активность.
Решения:
Ошибка:
client.subscribe('/topic/chat', callback);
client.subscribe('/topic/chat', callback);
Следствие:
Одно сообщение приходит дважды
Причина:
setInterval(...)
без очистки при уничтожении компонента.
Неправильная конфигурация может вызывать тысячи попыток подключения.
Рекомендуется:
reconnectDelay: 5000
вместо:
reconnectDelay: 1
Подключение:
const client = new Client({
brokerURL: 'ws://localhost:8080/chat',
reconnectDelay: 5000
});
Подписка:
client.onConn ect = () => {
client.subscribe('/topic/chat', message => {
const data = JSON.parse(message.body);
appendMessage(data);
});
};
Отправка:
function send(text) {
client.publish({
destination: '/app/chat',
body: JSON.stringify({
text,
author: 'Alex',
createdAt: Date.now()
})
});
}
Запуск:
client.activate();