В STOMP.js клиент может одновременно подписываться на большое количество каналов, очередей и пользовательских направлений. Такой подход используется в системах реального времени, где приложение получает данные из разных источников:
Множественные подписки позволяют одному WebSocket-соединению обслуживать сразу несколько потоков сообщений без необходимости создавать отдельный сокет для каждого канала.
Один экземпляр клиента может подписываться на любое количество destinations.
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws'
});
client.onConn ect = () => {
client.subscribe('/topic/news', (message) => {
console.log('Новости:', message.body);
});
client.subscribe('/topic/chat', (message) => {
console.log('Чат:', message.body);
});
client.subscribe('/queue/tasks', (message) => {
console.log('Задачи:', message.body);
});
};
client.activate();
После подключения STOMP.js создаёт несколько независимых подписок внутри одного TCP/WebSocket-соединения.
Каждая подписка обладает собственными параметрами:
Подписки не мешают друг другу.
client.subscribe('/topic/orders', onOrderMessage);
client.subscribe('/topic/payments', onPaymentMessage);
client.subscribe('/topic/errors', onErrorMessage);
Даже при ошибке в одном обработчике остальные подписки продолжают работать.
Метод subscribe() возвращает объект подписки.
const subscription = client.subscribe(
'/topic/events',
callback
);
Структура объекта:
{
id: 'sub-0',
unsubscribe: Function
}
Это позволяет управлять каждой подпиской отдельно.
При большом количестве каналов подписки обычно сохраняются в структуре данных.
const subscriptions = {};
subscriptions.news = client.subscribe(
'/topic/news',
onNews
);
subscriptions.chat = client.subscribe(
'/topic/chat',
onChat
);
subscriptions.tasks = client.subscribe(
'/queue/tasks',
onTasks
);
const subscriptions = new Map();
subscriptions.set(
'metrics',
client.subscribe('/topic/metrics', onMetrics)
);
subscriptions.set(
'alerts',
client.subscribe('/topic/alerts', onAlerts)
);
Map особенно удобен при динамическом управлении большим числом подписок.
Каждая подписка удаляется независимо.
const chatSubscription = client.subscribe(
'/topic/chat',
onChatMessage
);
chatSubscription.unsubscribe();
Остальные подписки продолжают работать.
Иногда требуется удалить сразу все подписки.
const subscriptions = [];
subscriptions.push(
client.subscribe('/topic/a', callbackA)
);
subscriptions.push(
client.subscribe('/topic/b', callbackB)
);
subscriptions.push(
client.subscribe('/topic/c', callbackC)
);
subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
Object.values(subscriptions).forEach((subscription) => {
subscription.unsubscribe();
});
Во многих приложениях каналы заранее неизвестны.
Например:
function subscribeToRoom(roomId) {
return client.subscribe(
`/topic/rooms/${roomId}`,
(message) => {
const data = JSON.parse(message.body);
console.log(roomId, data);
}
);
}
const roomSubscriptions = {};
const roomIds = [101, 102, 103];
roomIds.forEach((roomId) => {
roomSubscriptions[roomId] = client.subscribe(
`/topic/rooms/${roomId}`,
(message) => {
console.log(
`Комната ${roomId}:`,
message.body
);
}
);
});
function unsubscribeRoom(roomId) {
const subscription = roomSubscriptions[roomId];
if (!subscription) {
return;
}
subscription.unsubscribe();
delete roomSubscriptions[roomId];
}
Для множественных подписок часто создаются обработчики фабричным способом.
function createHandler(channel) {
return (message) => {
console.log(
`[${channel}]`,
message.body
);
};
}
client.subscribe(
'/topic/orders',
createHandler('orders')
);
client.subscribe(
'/topic/payments',
createHandler('payments')
);
В больших приложениях подписки группируются логически.
const chatModule = {
subscriptions: []
};
const notificationModule = {
subscriptions: []
};
const analyticsModule = {
subscriptions: []
};
chatModule.subscriptions.push(
client.subscribe('/topic/chat/global', onGlobalChat)
);
chatModule.subscriptions.push(
client.subscribe('/topic/chat/private', onPrivateChat)
);
function destroyModule(module) {
module.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
module.subscriptions = [];
}
Каждая подписка может иметь собственный режим подтверждения сообщений.
client.subscribe(
'/queue/orders',
onOrders,
{ ack: 'client' }
);
client.subscribe(
'/topic/logs',
onLogs,
{ ack: 'auto' }
);
Это позволяет:
Ошибка в callback не должна нарушать работу остальных подписок.
client.subscribe('/topic/data', (message) => {
const data = JSON.parse(message.body);
processData(data);
});
Если JSON повреждён — callback завершится исключением.
client.subscribe('/topic/data', (message) => {
try {
const data = JSON.parse(message.body);
processData(data);
} catch (error) {
console.error(error);
}
});
Количество подписок влияет на:
При сотнях подписок необходимо контролировать:
Частая ошибка — повторная подписка на один и тот же канал.
function init() {
client.subscribe('/topic/news', onNews);
}
Если init() вызывается многократно, создаются новые
подписки.
let newsSubscription = null;
function init() {
if (newsSubscription) {
return;
}
newsSubscription = client.subscribe(
'/topic/news',
onNews
);
}
После переподключения STOMP.js автоматически восстанавливает подписки, созданные через активный клиент.
Однако существуют особенности:
Иногда приложение полностью контролирует процесс reconnect.
const channels = [
'/topic/a',
'/topic/b',
'/topic/c'
];
client.onConn ect = () => {
channels.forEach((channel) => {
client.subscribe(channel, (message) => {
console.log(channel, message.body);
});
});
};
После нового подключения подписки создаются повторно.
Во многих системах используются персональные направления.
client.subscribe(
'/user/queue/notifications',
onNotification
);
Одновременно могут существовать:
client.subscribe('/user/queue/messages', onMessages);
client.subscribe('/user/queue/tasks', onTasks);
client.subscribe('/user/queue/events', onEvents);
Одно соединение может работать с разными типами destinations.
client.subscribe('/topic/system', onSystem);
client.subscribe('/queue/jobs', onJobs);
client.subscribe('/exchange/logs', onLogs);
Это особенно распространено в RabbitMQ STOMP Adapter.
STOMP требует уникальный id для каждой подписки.
STOMP.js генерирует идентификаторы автоматически:
sub-0
sub-1
sub-2
Но можно задавать собственные значения.
client.subscribe(
'/topic/news',
onNews,
{
id: 'news-subscription'
}
);
Ручные идентификаторы полезны:
client.subscribe(
'/topic/orders',
onOrders,
{
id: 'orders-v1'
}
);
Нельзя использовать одинаковый id для нескольких
подписок.
client.subscribe(
'/topic/a',
callbackA,
{ id: 'shared-id' }
);
client.subscribe(
'/topic/b',
callbackB,
{ id: 'shared-id' }
);
Результат зависит от брокера:
В React, Vue и Angular особенно важно удалять подписки при уничтожении компонентов.
function openPage() {
client.subscribe('/topic/page', callback);
}
При повторном открытии страницы количество подписок растёт.
let subscription = null;
function mount() {
subscription = client.subscribe(
'/topic/page',
callback
);
}
function unmount() {
if (subscription) {
subscription.unsubscribe();
}
}
В крупных проектах создаётся единый слой управления.
class SubscriptionManager {
constructor(client) {
this.client = client;
this.items = new Map();
}
subscribe(name, destination, callback) {
if (this.items.has(name)) {
return;
}
const subscription = this.client.subscribe(
destination,
callback
);
this.items.set(name, subscription);
}
unsubscribe(name) {
const subscription = this.items.get(name);
if (!subscription) {
return;
}
subscription.unsubscribe();
this.items.delete(name);
}
unsubscribeAll() {
this.items.forEach((subscription) => {
subscription.unsubscribe();
});
this.items.clear();
}
}
const manager = new SubscriptionManager(client);
manager.subscribe(
'orders',
'/topic/orders',
onOrders
);
manager.subscribe(
'payments',
'/topic/payments',
onPayments
);
manager.unsubscribe('orders');
При большом количестве каналов важно вести журнал операций.
function subscribeWithLog(destination, callback) {
console.log(
'SUBSCRIBE:',
destination
);
return client.subscribe(destination, callback);
}
console.log(
'Количество подписок:',
subscriptions.size
);
Это помогает находить:
Распространённая схема:
const subscriptions = {
system: [],
user: [],
chat: [],
analytics: []
};
Разделение по категориям упрощает:
const configs = [
{
key: 'orders',
destination: '/topic/orders',
handler: onOrders
},
{
key: 'payments',
destination: '/topic/payments',
handler: onPayments
},
{
key: 'alerts',
destination: '/topic/alerts',
handler: onAlerts
}
];
configs.forEach((config) => {
subscriptions[config.key] = client.subscribe(
config.destination,
config.handler
);
});
Брокеры сообщений могут ограничивать:
При высоких нагрузках используются:
Вместо множества подписок:
/topic/orders/created
/topic/orders/updated
/topic/orders/deleted
используется один канал:
/topic/orders
Тип события передаётся внутри сообщения:
{
"type": "updated",
"payload": {}
}
Такой подход уменьшает количество подписок.
Два крайних подхода:
Плюсы:
Минусы:
Плюсы:
Минусы:
Архитектура выбирается исходя из: