Vue.js и композиция

Архитектурная роль композиции в клиентских WebSocket-сценариях

Composition API в Vue 3 позволяет выносить поведение, связанное с подключением, подписками и обработкой сообщений STOMP.js, в переиспользуемые композиционные функции. Это критично для WebSocket-архитектур, где логика соединения должна быть отделена от компонентов интерфейса.

STOMP.js в таких системах выступает транспортным слоем поверх WebSocket, предоставляя модель pub/sub с брокером сообщений. При использовании вместе с Vue Composition API возникает типичная задача: изолировать жизненный цикл соединения, управление подписками и состояние соединения от UI-компонентов.


Базовая модель интеграции STOMP.js в Composition API

Ключевая идея заключается в создании composable-функции, которая инкапсулирует:

  • создание клиента STOMP
  • подключение и отключение
  • управление подписками
  • реактивное состояние соединения
  • обработку входящих сообщений

Структура composable

import { ref, shallowRef, onBeforeUnmount } from "vue";
import { Client } from "@stomp/stompjs";

export function useStomp(options) {
  const client = shallowRef(null);
  const connected = ref(false);
  const error = ref(null);

  const subscriptions = new Map();

  function connect() {
    client.value = new Client({
      brokerURL: options.url,
      reconnectDelay: 5000,
      heartbeatIncoming: 4000,
      heartbeatOutgoing: 4000,

      onConnect: () => {
        connected.value = true;
        error.value = null;

        if (options.onConnect) {
          options.onConnect();
        }
      },

      onStompError: (frame) => {
        error.value = frame;
      },

      onWebSocketClose: () => {
        connected.value = false;
      }
    });

    client.value.activate();
  }

  function disconnect() {
    if (client.value) {
      client.value.deactivate();
      client.value = null;
      connected.value = false;
    }
  }

  onBeforeUnmount(() => {
    disconnect();
  });

  return {
    client,
    connected,
    error,
    connect,
    disconnect
  };
}

Управление подписками внутри Composition API

STOMP предполагает работу с подписками через destination-каналы. В Composition API это удобно инкапсулировать через Map, где ключом выступает маршрут, а значением — объект подписки.

Реализация подписок

function subscribe(destination, callback) {
  if (!client.value || !connected.value) {
    throw new Error("STOMP client is not connected");
  }

  const subscription = client.value.subscribe(destination, (message) => {
    let parsed = message.body;

    try {
      parsed = JSON.parse(message.body);
    } catch (e) {
      // оставляем строку
    }

    callback(parsed, message);
  });

  subscriptions.set(destination, subscription);

  return () => unsubscribe(destination);
}

function unsubscribe(destination) {
  const sub = subscriptions.get(destination);

  if (sub) {
    sub.unsubscribe();
    subscriptions.delete(destination);
  }
}

Реактивная модель сообщений

Composition API позволяет строить реактивные потоки данных поверх STOMP сообщений.

Пример реактивного канала

import { ref } from "vue";

export function useChatChannel(stomp, roomId) {
  const messages = ref([]);

  function init() {
    stomp.subscribe(`/topic/chat/${roomId}`, (data) => {
      messages.value.push(data);
    });
  }

  function sendMessage(text) {
    stomp.client.value.publish({
      destination: `/app/chat/${roomId}`,
      body: JSON.stringify({ text })
    });
  }

  return {
    messages,
    init,
    sendMessage
  };
}

Разделение ответственности через composables

В больших приложениях STOMP слой не должен смешиваться с бизнес-логикой. Обычно выделяются три уровня:

1. Транспортный слой (useStomp)

Отвечает только за соединение и подписки.

2. Доменный слой (useChatChannel, useNotifications)

Определяет конкретные каналы и бизнес-семантику сообщений.

3. UI слой (Vue компоненты)

Подписывается на реактивные данные.


Управление переподключением и устойчивостью

STOMP.js поддерживает reconnectDelay, но этого недостаточно для сложных сценариев. В Composition API часто добавляется расширенная логика контроля состояния.

Расширенный контроль соединения

const retryCount = ref(0);
const maxRetries = 10;

function handleReconnect() {
  if (retryCount.value >= maxRetries) {
    error.value = "Max reconnect attempts reached";
    return;
  }

  retryCount.value++;
}

Подобная логика позволяет реализовать более строгий контроль стабильности соединения, чем стандартный механизм клиента.


Интеграция с Pinia

При использовании Pinia STOMP клиент часто выносится в глобальное хранилище, чтобы обеспечить единое соединение на всё приложение.

Пример store

import { defineStore } from "pinia";
import { Client } from "@stomp/stompjs";

export const useStompStore = defineStore("stomp", {
  state: () => ({
    client: null,
    connected: false
  }),

  actions: {
    init(url) {
      this.client = new Client({
        brokerURL: url,
        reconnectDelay: 5000,

        onConnect: () => {
          this.connected = true;
        },

        onWebSocketClose: () => {
          this.connected = false;
        }
      });

      this.client.activate();
    },

    subscribe(destination, callback) {
      return this.client.subscribe(destination, (msg) => {
        callback(JSON.parse(msg.body));
      });
    }
  }
});

Работа с lifecycle Vue компонентов

Composition API требует строгого соответствия жизненному циклу компонента и соединения STOMP.

Типовая схема

  • onMounted → connect / subscribe
  • onBeforeUnmount → unsubscribe / disconnect
import { onMounted, onBeforeUnmount } from "vue";

export function useRealtime(stomp) {
  function start() {
    stomp.connect();
  }

  function stop() {
    stomp.disconnect();
  }

  onMounted(start);
  onBeforeUnmount(stop);

  return {
    start,
    stop
  };
}

Оптимизация производительности подписок

При большом количестве каналов возникает проблема избыточных подписок. Composition API позволяет централизовать контроль.

Дедупликация подписок

const activeTopics = new Set();

function safeSubscribe(topic, handler) {
  if (activeTopics.has(topic)) {
    return;
  }

  activeTopics.add(topic);

  subscribe(topic, handler);
}

Обработка ошибок и деградация состояния

STOMP соединения должны учитывать:

  • разрыв WebSocket
  • ошибки брокера
  • некорректные сообщения

Унифицированная обработка

onStompError: (frame) => {
  error.value = {
    type: "STOMP_ERROR",
    message: frame.headers["message"]
  };
}

TypeScript-типизация в Composition API

В TypeScript-проектах STOMP слой становится строго типизированным.

type StompMessage<T> = {
  body: T;
  raw: any;
};

function subscribe<T>(
  destination: string,
  callback: (msg: StompMessage<T>) => void
) {
  return client.subscribe(destination, (message) => {
    callback({
      body: JSON.parse(message.body),
      raw: message
    });
  });
}

Изоляция бизнес-логики от транспорта

Одной из ключевых задач Composition API в связке со STOMP является предотвращение утечки транспортных деталей в UI.

UI не должен знать:

  • форматы STOMP frames
  • структуру broker URL
  • heartbeat настройки
  • reconnect policy

Вся эта информация инкапсулируется в composable, который предоставляет только:

  • данные
  • команды
  • реактивные состояния

Сценарий реального приложения: уведомления

export function useNotifications(stomp) {
  const notifications = ref([]);

  function init() {
    stomp.subscribe("/topic/notifications", (data) => {
      notifications.value.unshift(data);
    });
  }

  function markAsRead(id) {
    stomp.client.value.publish({
      destination: "/app/notifications/read",
      body: JSON.stringify({ id })
    });
  }

  return {
    notifications,
    init,
    markAsRead
  };
}

Композиционная масштабируемость

Composition API позволяет строить слой WebSocket-коммуникаций как набор независимых composables:

  • useStomp
  • useChat
  • useNotifications
  • usePresence
  • useActivityFeed

Каждый из них использует общий транспорт, но не знает о других доменах.

Такой подход формирует масштабируемую модель realtime-приложения, где STOMP.js выступает единым протоколом доставки сообщений, а Vue Composition API — механизмом структурирования поведения.