Обновление данных на странице в реальном времени — одна из ключевых задач при использовании WebSocket-приложений. Библиотека STOMP.js позволяет организовать постоянный поток сообщений между клиентом и сервером без необходимости выполнять регулярные HTTP-запросы.
Наиболее распространённые сценарии:
STOMP.js работает поверх WebSocket и позволяет подписываться на каналы, из которых сервер отправляет события клиенту.
Базовая схема работы выглядит следующим образом:
Установка через npm:
npm install @stomp/stompjs
Подключение клиента:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:8080/ws'
});
Активация соединения:
client.activate();
Самый простой вариант — обновление содержимого элемента при получении сообщения.
HTML:
<div id="status">Нет данных</div>
Jav * ascript:
import { Client } from '@stomp/stompjs';
const statusElement = document.getElementById('status');
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
onConnect: () => {
client.subscribe('/topic/status', message => {
statusElement.textContent = message.body;
});
}
});
client.activate();
При каждом новом сообщении содержимое блока автоматически изменяется.
Чаще всего сервер отправляет JSON.
Пример сообщения:
{
"name": "CPU",
"value": 74
}
Обработка:
client.subscribe('/topic/metrics', message => {
const data = JSON.parse(message.body);
document.getElementById('metric-name').textContent = data.name;
document.getElementById('metric-value').textContent = data.value;
});
HTML:
<div>
<span id="metric-name"></span>
<span id="metric-value"></span>
</div>
Один из наиболее распространённых сценариев — потоковое обновление списка.
HTML:
<ul id="messages"></ul>
Jav * ascript:
const messagesElement = document.getElementById('messages');
client.subscribe('/topic/chat', message => {
const data = JSON.parse(message.body);
const li = document.createElement('li');
li.textContent = data.text;
messagesElement.appendChild(li);
});
Каждое сообщение добавляется в DOM без перезагрузки страницы.
При длительной работе страницы количество элементов может стать слишком большим.
Пример ограничения:
const MAX_MESSAGES = 100;
client.subscribe('/topic/chat', message => {
const data = JSON.parse(message.body);
const li = document.createElement('li');
li.textContent = data.text;
messagesElement.appendChild(li);
while (messagesElement.children.length > MAX_MESSAGES) {
messagesElement.removeChild(messagesElement.firstChild);
}
});
Такой подход предотвращает переполнение DOM.
Иногда сервер отправляет полностью готовое состояние.
Пример:
client.subscribe('/topic/users', message => {
const users = JSON.parse(message.body);
const container = document.getElementById('users');
container.innerHTML = '';
users.forEach(user => {
const div = document.createElement('div');
div.textContent = user.name;
container.appendChild(div);
});
});
Этот вариант удобен для небольших наборов данных.
При больших объёмах данных выгоднее обновлять только изменившиеся элементы.
Структура HTML:
<div id="products"></div>
Обработка:
client.subscribe('/topic/product-update', message => {
const product = JSON.parse(message.body);
let row = document.getElementById(`product-${product.id}`);
if (!row) {
row = document.createElement('div');
row.id = `product-${product.id}`;
document.getElementById('products').appendChild(row);
}
row.textContent = `${product.name}: ${product.price}`;
});
Такой подход уменьшает нагрузку на браузер.
Для сложного интерфейса удобнее использовать шаблоны.
Функция рендеринга:
function renderUser(user) {
return `
<div class="user">
<div>${user.name}</div>
<div>${user.status}</div>
</div>
`;
}
Обновление:
client.subscribe('/topic/user-status', message => {
const user = JSON.parse(message.body);
const html = renderUser(user);
const container = document.getElementById('users');
container.insertAdjacentHTML('beforeend', html);
});
HTML:
<table>
<tbody id="table-body"></tbody>
</table>
Jav * ascript:
client.subscribe('/topic/orders', message => {
const order = JSON.parse(message.body);
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${order.id}</td>
<td>${order.status}</td>
<td>${order.price}</td>
`;
document.getElementById('table-body').appendChild(tr);
});
client.subscribe('/topic/order-update', message => {
const order = JSON.parse(message.body);
let row = document.getElementById(`order-${order.id}`);
if (!row) {
row = document.createElement('tr');
row.id = `order-${order.id}`;
document.getElementById('table-body').appendChild(row);
}
row.innerHTML = `
<td>${order.id}</td>
<td>${order.status}</td>
<td>${order.price}</td>
`;
});
HTML:
<div id="online-count">0</div>
Jav * ascript:
client.subscribe('/topic/online', message => {
const data = JSON.parse(message.body);
document.getElementById('online-count').textContent = data.count;
});
STOMP.js часто используется совместно с графическими библиотеками.
Пример с Chart.js:
client.subscribe('/topic/chart', message => {
const point = JSON.parse(message.body);
chart.data.labels.push(point.time);
chart.data.datasets[0].data.push(point.value);
chart.update();
});
HTML:
<div id="catalog"></div>
Jav * ascript:
client.subscribe('/topic/catalog', message => {
const product = JSON.parse(message.body);
let card = document.getElementById(`card-${product.id}`);
if (!card) {
card = document.createElement('div');
card.id = `card-${product.id}`;
document.getElementById('catalog').appendChild(card);
}
card.innerHTML = `
<h3>${product.name}</h3>
<p>${product.price}</p>
<p>${product.stock}</p>
`;
});
При высокой частоте сообщений возникает проблема постоянного перерисовывания DOM.
Плохой пример:
client.subscribe('/topic/live', message => {
element.textContent = message.body;
});
Если сервер отправляет сотни сообщений в секунду, браузер начнёт тормозить.
Эффективный способ — накопление сообщений.
const queue = [];
client.subscribe('/topic/live', message => {
queue.push(JSON.parse(message.body));
});
Обработка очереди:
setInterval(() => {
if (queue.length === 0) {
return;
}
const batch = queue.splice(0, queue.length);
batch.forEach(item => {
updateUI(item);
});
}, 100);
let latestData = null;
client.subscribe('/topic/live', message => {
latestData = JSON.parse(message.body);
});
function render() {
if (latestData) {
updateUI(latestData);
latestData = null;
}
requestAnimationFrame(render);
}
render();
Такой подход синхронизирует обновления с циклом рендеринга браузера.
Частая ошибка:
container.innerHTML += `
<div>${message.body}</div>
`;
Недостатки:
Предпочтительный вариант:
const div = document.createElement('div');
div.textContent = message.body;
container.appendChild(div);
Для массового обновления DOM:
client.subscribe('/topic/bulk', message => {
const items = JSON.parse(message.body);
const fragment = document.createDocumentFragment();
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item.name;
fragment.appendChild(div);
});
container.appendChild(fragment);
});
Если компонент удаляется со страницы, подписку необходимо закрывать.
const subscription = client.subscribe('/topic/chat', callback);
subscription.unsubscribe();
Без этого возможны:
При потере соединения данные могут перестать обновляться.
STOMP.js поддерживает автоматический reconnect.
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
reconnectDelay: 5000
});
После восстановления соединения подписки создаются заново внутри
onConnect.
onConnect: () => {
client.subscribe('/topic/data', callback);
}
HTML:
<div id="connection-status"></div>
Jav * ascript:
const statusElement = document.getElementById('connection-status');
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
onConnect: () => {
statusElement.textContent = 'Connected';
},
onDisconnect: () => {
statusElement.textContent = 'Disconnected';
},
onStompError: frame => {
statusElement.textContent = 'Error';
}
});
Иногда сервер отправляет слишком много одинаковых событий.
Пример debounce:
let timeout = null;
client.subscribe('/topic/search', message => {
const data = JSON.parse(message.body);
clearTimeout(timeout);
timeout = setTimeout(() => {
render(data);
}, 300);
});
Нежелательно использовать DOM как источник данных.
Плохой подход:
const current = element.textContent;
Лучше хранить состояние отдельно:
const state = {
users: []
};
Обновление:
client.subscribe('/topic/users', message => {
state.users = JSON.parse(message.body);
renderUsers(state.users);
});
useEffect(() => {
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
onConnect: () => {
client.subscribe('/topic/messages', message => {
const data = JSON.parse(message.body);
setMessages(prev => [...prev, data]);
});
}
});
client.activate();
return () => {
client.deactivate();
};
}, []);
onMounted(() => {
client.activate();
});
onUnmounted(() => {
client.deactivate();
});
Подписка:
client.subscribe('/topic/data', message => {
items.value.push(JSON.parse(message.body));
});
this.client.subscribe('/topic/data', message => {
const data = JSON.parse(message.body);
this.items.push(data);
});
При огромных объёмах данных полезно отображать только видимую часть списка.
Проблемы без виртуализации:
В таких случаях используются:
Одно сообщение может обновлять разные части страницы.
client.subscribe('/topic/dashboard', message => {
const data = JSON.parse(message.body);
updateChart(data.chart);
updateTable(data.table);
updateStats(data.stats);
});
Для крупных приложений полезно разделять транспорт и UI.
client.subscribe('/topic/events', message => {
const event = JSON.parse(message.body);
eventBus.emit(event.type, event.payload);
});
Обработчики:
eventBus.on('USER_UPDATED', updateUser);
eventBus.on('NEW_ORDER', addOrder);
eventBus.on('PRICE_CHANGED', updatePrice);
Основные рекомендации:
Структура приложения:
WebSocket
↓
STOMP.js
↓
Message Handler
↓
State Store
↓
UI Renderer
↓
DOM
Такой подход обеспечивает: