Миграция между версиями STOMP.js обычно связана с несколькими факторами:
Наиболее заметные изменения произошли при переходе:
stompjs на
@stomp/stompjs;Ранние версии библиотеки использовали пакет:
npm install stompjs
Импорт выглядел следующим образом:
import Stomp from 'stompjs';
Современная версия использует пакет:
npm install @stomp/stompjs
Новый импорт:
import { Client } from '@stomp/stompjs';
| Старый API | Новый API |
|---|---|
Stomp.client() |
new Client() |
| Callback connect | Конфигурация клиента |
reconnect_delay |
reconnectDelay |
| Частичная типизация | Полная поддержка TypeScript |
| Глобальный объект | ES-модули |
При крупных проектах резкая замена всех механизмов подключения может привести к нестабильности. Намного безопаснее использовать поэтапный подход.
Перед обновлением рекомендуется вынести работу с STOMP в отдельный сервис.
Плохой вариант:
const socket = new WebSocket(url);
const client = Stomp.over(socket);
client.connect({}, () => {
client.subscribe('/topic/chat', message => {
console.log(message.body);
});
});
STOMP-логика распределена по приложению.
Хороший вариант:
export class MessagingService {
connect() {}
disconnect() {}
subscribe() {}
publish() {}
}
После изоляции миграция становится локальной задачей.
При большом количестве legacy-кода полезно создавать промежуточный слой совместимости.
client.send('/topic/test', {}, JSON.stringify(data));
client.publish({
destination: '/topic/test',
body: JSON.stringify(data)
});
class LegacyAdapter {
constructor(client) {
this.client = client;
}
send(destination, headers, body) {
this.client.publish({
destination,
headers,
body
});
}
}
Такой подход позволяет обновлять систему постепенно.
const socket = new WebSocket(url);
const client = Stomp.over(socket);
client.connect(
login,
password,
onConnect,
onError
);
const client = new Client({
brokerURL: url,
connectHeaders: {
login,
passcode: password
},
onConnect: () => {
console.log('Connected');
},
onStompError: frame => {
console.log(frame);
}
});
Современная архитектура строится вокруг объекта конфигурации.
Вместо большого числа callback-функций используются обработчики событий:
onConnect
onDisconnect
onWebSocketClose
onWebSocketError
onUnhandledMessage
onUnhandledReceipt
Раньше соединение создавалось через connect().
Теперь:
client.activate();
Отключение:
client.deactivate();
client.subscribe('/queue/test', callback);
client.subscribe('/queue/test', message => {
console.log(message.body);
});
На первый взгляд код почти идентичен, однако поведение acknowledgement и lifecycle отличается.
Старые проекты часто используют implicit ack.
client.subscribe('/queue/tasks', message => {
processTask(message);
});
client.subscribe(
'/queue/tasks',
message => {
processTask(message);
message.ack();
},
{
ack: 'client'
}
);
Implicit ack опасен:
client.reconnect_delay = 5000;
const client = new Client({
reconnectDelay: 5000
});
Современные версии поддерживают:
Автоматический reconnect может привести к лавинообразным подключениям.
Плохой вариант:
reconnectDelay: 100
Это создаёт:
Более безопасная стратегия:
reconnectDelay: 5000
Или динамический backoff:
let reconnectDelay = 1000;
client.onWebSocketCl ose = () => {
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
client.configure({
reconnectDelay
});
};
client.heartbeat.outgoing = 20000;
client.heartbeat.incoming = 0;
const client = new Client({
heartbeatIncoming: 0,
heartbeatOutgoing: 20000
});
client.send(
'/topic/chat',
{
priority: 9
},
JSON.stringify(data)
);
client.publish({
destination: '/topic/chat',
headers: {
priority: 9
},
body: JSON.stringify(data)
});
Старые версии STOMP.js были ориентированы преимущественно на текстовые payload.
Современные версии поддерживают бинарные данные.
client.publish({
destination: '/queue/binary',
binaryBody: uint8Array
});
Обновление выполняется по модулям:
Полная замена STOMP-слоя сразу во всём проекте.
Для production-систем обычно безопаснее горизонтальный подход.
client.connect({}, function() {
client.subscribe('/topic/a', function(msg) {
console.log(msg.body);
});
});
const client = new Client({
brokerURL: url,
onConnect: () => {
client.subscribe('/topic/a', msg => {
console.log(msg.body);
});
}
});
Одним из главных преимуществ современных версий является встроенная типизация.
client.publish({
destination: '/topic/test',
body: data
});
client.publish({
destination: '/topic/test',
body: JSON.stringify(data)
});
interface ChatMessage {
id: number;
text: string;
author: string;
}
Использование DTO снижает вероятность ошибок сериализации.
client.connect({}, onConnect, onError);
const client = new Client({
onStompError: frame => {
console.log(frame.headers['message']);
},
onWebSocketError: event => {
console.log(event);
}
});
Современная архитектура различает:
| Тип ошибки | Назначение |
|---|---|
onStompError |
Ошибки STOMP-протокола |
onWebSocketError |
Ошибки транспорта |
onWebSocketClose |
Закрытие соединения |
onDisconnect |
Корректное отключение |
Иногда обновление STOMP.js сопровождается переходом между брокерами:
Некоторые брокеры:
client-individual;Примеры:
/topic/chat
/queue/tasks
/exchange/logs
/amq/queue/test
Во время миграции рекомендуется создавать abstraction layer.
В критически важных системах иногда временно используются два клиента.
const legacyClient = createLegacyClient();
const modernClient = createModernClient();
Часть пользователей переводится на новую версию постепенно.
if (featureFlags.newStompClient) {
useModernClient();
} else {
useLegacyClient();
}
Используются две независимые среды.
Старая STOMP-инфраструктура.
Новая версия STOMP.js и новый messaging layer.
После проверки трафик переключается полностью.
Любая миграция должна предусматривать откат.
Нельзя удалять поддержку старого формата сообщений слишком рано.
Пример:
{
"version": 2,
"payload": {}
}
Бизнес-логика не должна зависеть от реализации STOMP-клиента.
Современные приложения часто интегрируют STOMP.js с RxJS.
import { Observable } from 'rxjs';
function observeTopic(client, topic) {
return new Observable(subscriber => {
const subscription = client.subscribe(
topic,
message => subscriber.next(message)
);
return () => subscription.unsubscribe();
});
}
Особое внимание требуется для:
Создание нового STOMP-клиента при каждом рендере компонента.
Плохой вариант:
function Chat() {
const client = new Client();
}
Singleton или service container.
export const stompClient = new Client();
Старые системы часто используют login/passcode.
Современные приложения переходят на:
const client = new Client({
connectHeaders: {
Authorization: `Bearer ${token}`
}
});
При использовании Spring Framework часто встречается переход:
/sockjs
к полноценному WebSocket:
/ws
Проверяется:
Проверяется:
Проверяются:
При наличии множества frontend и backend сервисов миграция усложняется.
Frontend → Gateway → Broker → Services
/topic/v1/chat
/topic/v2/chat
{
"version": 1
}
{
"version": 2
}
import SockJS from 'sockjs-client';
const socket = new SockJS(url);
const client = Stomp.over(socket);
const client = new Client({
webSocketFactory: () => {
return new SockJS(url);
}
});
Ранние версии часто использовали mutation-style конфигурирование.
client.debug = null;
client.reconnect_delay = 5000;
const client = new Client({
debug: () => {},
reconnectDelay: 5000
});
Старые свойства:
reconnect_delay
Новые свойства:
reconnectDelay
Создание compatibility normalizer:
function normalize(config) {
return {
reconnectDelay: config.reconnect_delay
};
}
В монолитных frontend-приложениях STOMP-код часто смешан с UI.
button.oncl ick = () => {
client.send(...);
};
Разделение:
Современные приложения часто интегрируют STOMP.js с:
client.subscribe('/topic/chat', message => {
store.dispatch({
type: 'MESSAGE_RECEIVED',
payload: JSON.parse(message.body)
});
});
После миграции могут появиться ошибки:
global is not defined
Настройка bundler environment.
После завершения миграции удаляются:
Слишком раннее удаление legacy-слоя часто приводит к production-регрессиям, поэтому очистка инфраструктуры выполняется только после длительного периода стабильной эксплуатации новой версии.