1. 引言

在现代前端应用中,实时通信已成为不可或缺的功能。STOMP(Simple Text Oriented Messaging Protocol)作为 WebSocket 的子协议,为消息传递提供了清晰的帧格式和语义。@stomp/stompjs 是一个优秀的 STOMP 客户端库,但在实际 Vue 3 项目中直接使用仍显繁琐。

本文将介绍一个基于 @stomp/stompjs 的 Vue 3 Composition API 封装,它解决了以下痛点:

  • URL 热切换:当 WebSocket 地址变更时自动断开旧连接并建立新连接
  • 自动重订阅:连接地址变更后自动恢复所有之前的订阅
  • 响应式状态管理:连接状态可被 Vue 组件监听
  • 模块级单例:避免重复创建客户端实例

2. 核心设计思路

2.1 模块级单例模式

封装采用模块级单例设计,确保整个应用只有一个 STOMP 客户端实例。这避免了资源浪费和状态不一致问题。

// ---- 模块级单例 ----
let stompClient: Client | null = null;
let currentBrokerUrl: string | null = null;
let connectPromise: Promise<void> | null = null;

2.2 响应式连接状态

使用 Vue 的 ref 管理连接状态,便于组件监听状态变化:

/** 连接状态 */
type ConnectionStatus = "disconnected" | "connecting" | "connected";

/** 响应式连接状态 */
const connectionStatus = ref<ConnectionStatus>("disconnected");

2.3 订阅管理

维护两个映射表来管理订阅:

  • subscriptionMap:当前活跃的订阅
  • subscriptionCallbacks:保存的订阅回调(用于 URL 变更后重订阅)

3. 核心实现解析

3.1 客户端获取与 URL 热切换

getClient() 函数是封装的核心,负责处理 URL 变更:

function getClient(): Client {
  const wsStore = useWebsocketStore();
  const newUrl = wsStore.wsUrlNew;
  
  // URL 没变且 Client 已存在,直接复用
  if (stompClient && currentBrokerUrl === newUrl) {
    return stompClient;
  }
  
  // URL 变了或首次创建 → 销毁旧 Client,建新的
  if (stompClient) {
    console.log(`[StompClient] URL 变更: ${currentBrokerUrl} → ${newUrl}`);
    try {
      stompClient.deactivate();
    } catch {
      // 忽略
    }
    stompClient = null;
    connectionStatus.value = "disconnected";
    connectPromise = null;
  }
  
  currentBrokerUrl = newUrl;
  const hadSubscriptions = subscriptionCallbacks.size > 0;
  
  stompClient = new Client({
    brokerURL: newUrl,
    reconnectDelay: 0,
    // ... 其他配置
  });
  
  return stompClient;
}

3.2 自动重订阅机制

在 onConnect 回调中实现自动重订阅:

onConnect: () => {
  connectionStatus.value = "connected";
  console.log(`[StompClient] 已连接 → ${newUrl}`);
  onConnectCallback?.();
  
  // URL 变更后自动重订阅之前的 topic
  if (hadSubscriptions) {
    const topics = [...subscriptionCallbacks.keys()];
    subscriptionMap.clear();
    topics.forEach((topic) => {
      const cb = subscriptionCallbacks.get(topic);
      if (cb) {
        const sub = stompClient!.subscribe(topic, (message) => {
          let body: any = message.body;
          try {
            body = JSON.parse(message.body);
          } catch { }
          cb(body, message.headers as Record<string, string>);
        });
        subscriptionMap.set(topic, sub);
        console.log(`[StompClient] 自动重订阅: ${topic}`);
      }
    });
  }
},

3.3 连接管理

connect() 函数实现幂等连接,确保同一时间只有一个连接过程:

async function connect(): Promise<void> {
  // 必须在状态检查前调用 getClient(),以检测 URL 变更
  const client = getClient();
  
  if (connectionStatus.value === "connected") return Promise.resolve();
  if (connectPromise) return connectPromise;
  
  connectionStatus.value = "connecting";
  connectPromise = new Promise<void>((resolve, reject) => {
    // ... 连接逻辑
  });
  
  return connectPromise;
}

4. 完整 API 使用示例

4.1 基本用法

import { useStompClient } from '@/utils/stomp-client';

// 在组件中
const { connect, subscribe, send, disconnect, connectionStatus } = useStompClient({
  onConnect: () => console.log("连接成功"),
  onDisconnect: () => console.log("连接断开"),
});

// 连接
await connect();

// 订阅主题
const subscription = subscribe("/topic/notifications", (body, headers) => {
  console.log("收到消息:", body);
  console.log("消息头:", headers);
});

// 发送消息
send("/app/chat", { text: "Hello World" });

// 取消订阅
subscription.unsubscribe();

// 断开连接
await disconnect();

4.2 在 Vue 组件中使用

<template>
  <div>
    <div>连接状态: {{ statusText }}</div>
    <button @click="toggleConnection" :disabled="isConnecting">
      {{ isConnected ? '断开连接' : '连接' }}
    </button>
    <div v-if="messages.length">
      <h3>收到的消息:</h3>
      <ul>
        <li v-for="(msg, index) in messages" :key="index">{{ msg }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue';
import { useStompClient } from '@/utils/stomp-client';

const messages = ref<string[]>([]);
const { connect, disconnect, subscribe, connectionStatus } = useStompClient();

const isConnected = computed(() => connectionStatus.value === 'connected');
const isConnecting = computed(() => connectionStatus.value === 'connecting');
const statusText = computed(() => {
  switch (connectionStatus.value) {
    case 'connected': return '已连接';
    case 'connecting': return '连接中';
    default: return '未连接';
  }
});

// 连接/断开连接
const toggleConnection = async () => {
  if (isConnected.value) {
    await disconnect();
    messages.value = [];
  } else {
    await connect();
    subscribe('/topic/messages', (body) => {
      messages.value.push(body.content);
    });
  }
};
</script>

4.3 API 函数详细说明

useStompClient(options?: { onConnect?: () => void; onDisconnect?: () => void })

创建 STOMP 客户端实例的 Composition API hook,支持配置连接/断开回调。

参数说明:

interface StompClientOptions {
  // 连接成功时的回调函数
  onConnect?: () => void;
  
  // 连接断开时的回调函数
  onDisconnect?: () => void;
}

使用示例:

// 1. 基本用法(无参数)
const { connect, disconnect } = useStompClient();

// 2. 带连接/断开回调
const { connect, disconnect } = useStompClient({
  onConnect: () => {
    console.log('STOMP 连接成功');
    // 可以在这里执行连接成功后的初始化操作
    showNotification('WebSocket 已连接', 'success');
  },
  onDisconnect: () => {
    console.log('STOMP 连接断开');
    // 可以在这里执行连接断开后的清理操作
    showNotification('WebSocket 已断开', 'warning');
  }
});

// 3. 在组件中使用
import { useStompClient } from '@/utils/stomp-client';
import { onUnmounted } from 'vue';

export default {
  setup() {
    // 创建客户端实例
    const stompClient = useStompClient({
      onConnect: () => {
        console.log('组件内连接成功');
        // 连接成功后自动订阅
        stompClient.subscribe('/topic/updates', handleUpdates);
      },
      onDisconnect: () => {
        console.log('组件内连接断开');
        // 清理组件相关状态
      }
    });

    // 组件卸载时断开连接
    onUnmounted(async () => {
      if (stompClient.isConnected()) {
        await stompClient.disconnect();
      }
    });

    return { stompClient };
  }
};

注意事项:

  1. onConnect 和 onDisconnect 回调是全局的,所有使用同一个 hook 实例的地方都会触发
  2. 回调函数会在对应的连接状态变化时同步执行
  3. 建议在需要处理连接状态变化的组件中配置回调
  4. 回调函数中避免执行耗时操作,以免阻塞连接流程

useStompClient() 返回一个包含以下函数和属性的对象:

connectionStatus: Ref<ConnectionStatus>

响应式连接状态引用,可用于在 Vue 组件中实时监听连接状态变化。

// 在组件中使用
const { connectionStatus } = useStompClient();

// 监听状态变化
watch(connectionStatus, (newStatus) => {
  console.log('连接状态变化:', newStatus);
});

// 计算属性
const isConnected = computed(() => connectionStatus.value === 'connected');
const isConnecting = computed(() => connectionStatus.value === 'connecting');
const isDisconnected = computed(() => connectionStatus.value === 'disconnected');

connect(): Promise<void>

建立 STOMP 连接,支持幂等调用(多次调用不会重复连接)。

const { connect } = useStompClient();

// 基本用法
await connect();

// 错误处理
try {
  await connect();
  console.log('连接成功');
} catch (error) {
  console.error('连接失败:', error);
}

// 幂等性:多次调用只建立一次连接
await connect(); // 第一次调用,建立连接
await connect(); // 第二次调用,直接返回(不会重复连接)

disconnect(): Promise<void>

断开 STOMP 连接,清理所有订阅。

const { disconnect } = useStompClient();

// 基本用法
await disconnect();

// 配合连接状态使用
const { connect, disconnect, connectionStatus } = useStompClient();

const toggleConnection = async () => {
  if (connectionStatus.value === 'connected') {
    await disconnect();
    console.log('已断开连接');
  } else {
    await connect();
    console.log('已连接');
  }
};

// 断开连接时会自动清理所有订阅
await disconnect();
// 此时 subscriptionMap 和 subscriptionCallbacks 都会被清空

subscribe(topic: string, callback: MessageCallback): StompSubscription

订阅指定主题,返回订阅对象用于后续取消订阅。

const { subscribe } = useStompClient();

// 基本用法
const subscription = subscribe('/topic/notifications', (body, headers) => {
  console.log('收到消息:', body);
  console.log('消息头:', headers);
});

// 处理 JSON 消息
subscribe('/topic/data', (body) => {
  // body 已自动尝试 JSON.parse,如果失败则保持原字符串
  if (typeof body === 'object') {
    console.log('JSON 数据:', body.id, body.content);
  } else {
    console.log('文本数据:', body);
  }
});

// 订阅多个主题
const sub1 = subscribe('/topic/user/123', handleUserMessages);
const sub2 = subscribe('/topic/system', handleSystemMessages);

unsubscribe(topic: string): void

取消订阅指定主题。

const { subscribe, unsubscribe } = useStompClient();

// 方式1:使用 subscribe 返回的 subscription 对象
const subscription = subscribe('/topic/test', callback);
subscription.unsubscribe(); // 取消订阅

// 方式2:直接使用 unsubscribe 函数
subscribe('/topic/test', callback);
unsubscribe('/topic/test'); // 取消订阅

// 批量取消订阅
const topics = ['/topic/a', '/topic/b', '/topic/c'];
topics.forEach(topic => subscribe(topic, callback));

// 取消所有订阅
topics.forEach(topic => unsubscribe(topic));

send(destination: string, body: any, headers?: Record<string, string>): void

向指定目的地发送消息。

const { send } = useStompClient();

// 发送简单消息
send('/app/chat', { text: 'Hello World' });

// 发送带 headers 的消息
send('/app/command', 
  { command: 'start', params: { id: 123 } },
  { 'content-type': 'application/json', 'priority': 'high' }
);

// 发送字符串消息
send('/app/log', '这是一条日志消息');

// 发送前确保已连接
const { connect, send, connectionStatus } = useStompClient();

const sendMessage = async (destination: string, body: any) => {
  if (connectionStatus.value !== 'connected') {
    await connect();
  }
  send(destination, body);
};

isConnected(): boolean

检查当前是否已连接(同步函数)。

const { isConnected, connect } = useStompClient();

// 同步检查连接状态
if (isConnected()) {
  console.log('当前已连接');
  send('/app/ping', { time: Date.now() });
} else {
  console.log('当前未连接');
  await connect();
}

// 与响应式 connectionStatus 的区别
const { isConnected, connectionStatus } = useStompClient();

// isConnected() 是同步函数,返回当前布尔值
console.log(isConnected()); // true/false

// connectionStatus 是响应式引用,适合模板中使用
// 模板中:<div>状态: {{ connectionStatus }}</div>

isSubscribed(topic: string): boolean

检查是否已订阅指定主题。

const { subscribe, isSubscribed } = useStompClient();

// 订阅前检查
if (!isSubscribed('/topic/important')) {
  subscribe('/topic/important', handleImportantMessages);
}

// 避免重复订阅
const topic = '/topic/updates';
if (!isSubscribed(topic)) {
  subscribe(topic, handleUpdates);
}

// 配合 unsubscribe 使用
const toggleSubscription = (topic: string, callback: MessageCallback) => {
  if (isSubscribed(topic)) {
    unsubscribe(topic);
    console.log(`已取消订阅 ${topic}`);
  } else {
    subscribe(topic, callback);
    console.log(`已订阅 ${topic}`);
  }
};

完整类型定义

export function useStompClient(options?: {
  onConnect?: () => void;
  onDisconnect?: () => void;
}): {
  // 响应式状态
  connectionStatus: Ref<ConnectionStatus>;
  
  // 连接管理
  connect: () => Promise<void>;
  disconnect: () => Promise<void>;
  
  // 订阅管理
  subscribe: (topic: string, callback: MessageCallback) => StompSubscription;
  unsubscribe: (topic: string) => void;
  
  // 消息发送
  send: (destination: string, body: any, headers?: Record<string, string>) => void;
  
  // 状态检查
  isConnected: () => boolean;
  isSubscribed: (topic: string) => boolean;
};

5. 与 Pinia 状态管理集成

5.1 WebSocket Store 设计

// stores/websocket.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useWebsocketStore = defineStore('websocket', () => {
  // 连接指定ip
  const wsUrlNew = ref<string>('');
  
  const setWsUrlNew = (url: string) => {
    wsUrlNew.value = url;
    console.log('[WebSocket Store] 地址已更新:', url.length > 0 ? url : '地址为空');
  };
  
  return {
    wsUrlNew,
    setWsUrlNew,
  };
});

5.2 URL 变更触发重连

当 Pinia store 中的 wsUrlNew 发生变化时,下次调用 connect() 会自动:

  1. 检测到 URL 变更
  2. 断开旧连接
  3. 建立新连接
  4. 自动重订阅所有之前的主题

6. 错误处理与调试

6.1 错误处理

try {
  await connect();
} catch (error) {
  console.error('连接失败:', error);
  // 显示用户友好的错误信息
  showErrorMessage(`WebSocket 连接失败: ${error.message}`);
}

// 配置错误回调
const { connect, subscribe } = useStompClient({
  onConnect: () => console.log('连接成功'),
  onDisconnect: () => console.log('连接断开'),
  // 可以扩展支持 onError 回调
});

6.2 调试日志

封装内置了详细的调试日志,可通过环境变量控制:

const DEBUG = import.meta.env.DEV;

// 在关键位置添加条件日志
if (DEBUG) {
  console.log(`[StompClient] URL 变更: ${currentBrokerUrl} → ${newUrl}`);
  console.log(`[StompClient] 自动重订阅: ${topic}`);
}

7. 性能优化建议

7.1 防抖连接

避免频繁调用 connect():

import { debounce } from 'lodash-es';

const debouncedConnect = debounce(async () => {
  try {
    await connect();
  } catch (error) {
    console.error('连接失败:', error);
  }
}, 1000);

// 在 URL 变化时调用
watch(() => wsStore.wsUrlNew, debouncedConnect);

7.2 心跳检测

配置心跳机制保持连接活跃:

stompClient = new Client({
  brokerURL: newUrl,
  reconnectDelay: 5000, // 5秒重连延迟
  heartbeatIncoming: 10000, // 10秒接收心跳
  heartbeatOutgoing: 10000, // 10秒发送心跳
  // ... 其他配置
});

8. 总结

本文介绍的 STOMP 客户端封装具有以下优势:

  1. URL 热切换:无需手动处理连接重建,地址变更自动处理
  2. 状态保持:连接断开后自动恢复所有订阅
  3. 响应式集成:完美融入 Vue 3 响应式系统
  4. 类型安全:完整的 TypeScript 类型支持
  5. 易于扩展:模块化设计便于添加新功能

这个封装特别适合需要动态切换 WebSocket 服务器地址的应用场景,如多租户系统、环境切换、故障转移等场景。

9. 完整代码

以下是完整的 STOMP 客户端封装代码,包含所有核心功能和与 Pinia 的集成:

9.1 STOMP 客户端封装 (stomp-client.ts)

// src/utils/stomp-client.ts
import { Client, IMessage, StompSubscription } from '@stomp/stompjs';
import { ref, Ref } from 'vue';
import { useWebsocketStore } from '@/stores/websocket';

/** 连接状态类型 */
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';

/** 消息回调函数类型 */
type MessageCallback = (body: any, headers: Record<string, string>) => void;

// ---- 模块级单例 ----
let stompClient: Client | null = null;
let currentBrokerUrl: string | null = null;
let connectPromise: Promise<void> | null = null;

// ---- 响应式状态 ----
/** 响应式连接状态 */
const connectionStatus = ref<ConnectionStatus>('disconnected');

// ---- 订阅管理 ----
/** 当前活跃的订阅映射 */
const subscriptionMap = new Map<string, StompSubscription>();
/** 保存的订阅回调(用于 URL 变更后重订阅) */
const subscriptionCallbacks = new Map<string, MessageCallback>();

// ---- 回调函数 ----
let onConnectCallback: (() => void) | null = null;
let onDisconnectCallback: (() => void) | null = null;

/**
 * 获取 STOMP 客户端实例(支持 URL 热切换)
 */
function getClient(): Client {
  const wsStore = useWebsocketStore();
  const newUrl = wsStore.wsUrlNew;
  
  // URL 没变且 Client 已存在,直接复用
  if (stompClient && currentBrokerUrl === newUrl) {
    return stompClient;
  }
  
  // URL 变了或首次创建 → 销毁旧 Client,建新的
  if (stompClient) {
    console.log(`[StompClient] URL 变更: ${currentBrokerUrl} → ${newUrl}`);
    try {
      stompClient.deactivate();
    } catch {
      // 忽略
    }
    stompClient = null;
    connectionStatus.value = 'disconnected';
    connectPromise = null;
  }
  
  currentBrokerUrl = newUrl;
  const hadSubscriptions = subscriptionCallbacks.size > 0;
  
  stompClient = new Client({
    brokerURL: newUrl,
    reconnectDelay: 0,
    heartbeatIncoming: 10000,
    heartbeatOutgoing: 10000,
    debug: (str) => {
      if (import.meta.env.DEV) {
        console.log('[STOMP]', str);
      }
    },
    onConnect: () => {
      connectionStatus.value = 'connected';
      console.log(`[StompClient] 已连接 → ${newUrl}`);
      onConnectCallback?.();
      
      // URL 变更后自动重订阅之前的 topic
      if (hadSubscriptions) {
        const topics = [...subscriptionCallbacks.keys()];
        subscriptionMap.clear();
        topics.forEach((topic) => {
          const cb = subscriptionCallbacks.get(topic);
          if (cb) {
            const sub = stompClient!.subscribe(topic, (message: IMessage) => {
              let body: any = message.body;
              try {
                body = JSON.parse(message.body);
              } catch {
                // 保持原字符串
              }
              cb(body, message.headers as Record<string, string>);
            });
            subscriptionMap.set(topic, sub);
            console.log(`[StompClient] 自动重订阅: ${topic}`);
          }
        });
      }
    },
    onDisconnect: () => {
      connectionStatus.value = 'disconnected';
      console.log('[StompClient] 连接断开');
      onDisconnectCallback?.();
    },
    onStompError: (frame) => {
      console.error('[StompClient] STOMP 错误:', frame.headers['message'], frame.body);
    },
    onWebSocketError: (event) => {
      console.error('[StompClient] WebSocket 错误:', event);
      connectionStatus.value = 'disconnected';
    },
  });
  
  return stompClient;
}

/**
 * 创建 STOMP 客户端实例的 Composition API hook
 */
export function useStompClient(options?: {
  onConnect?: () => void;
  onDisconnect?: () => void;
}) {
  // 设置回调函数
  if (options?.onConnect) {
    onConnectCallback = options.onConnect;
  }
  if (options?.onDisconnect) {
    onDisconnectCallback = options.onDisconnect;
  }
  
  /**
   * 建立 STOMP 连接(幂等)
   */
  async function connect(): Promise<void> {
    // 必须在状态检查前调用 getClient(),以检测 URL 变更
    const client = getClient();
    
    if (connectionStatus.value === 'connected') return Promise.resolve();
    if (connectPromise) return connectPromise;
    
    connectionStatus.value = 'connecting';
    connectPromise = new Promise<void>((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        connectionStatus.value = 'disconnected';
        connectPromise = null;
        reject(new Error('连接超时'));
      }, 10000);
      
      client.onConnect = () => {
        clearTimeout(timeoutId);
        resolve();
      };
      
      client.onStompError = (frame) => {
        clearTimeout(timeoutId);
        connectionStatus.value = 'disconnected';
        connectPromise = null;
        reject(new Error(frame.headers['message'] || 'STOMP 连接错误'));
      };
      
      client.onWebSocketError = (event) => {
        clearTimeout(timeoutId);
        connectionStatus.value = 'disconnected';
        connectPromise = null;
        reject(new Error('WebSocket 连接错误'));
      };
      
      try {
        client.activate();
      } catch (error) {
        clearTimeout(timeoutId);
        connectionStatus.value = 'disconnected';
        connectPromise = null;
        reject(error);
      }
    });
    
    return connectPromise;
  }
  
  /**
   * 断开 STOMP 连接
   */
  async function disconnect(): Promise<void> {
    if (!stompClient || connectionStatus.value === 'disconnected') {
      return Promise.resolve();
    }
    
    try {
      stompClient.deactivate();
      connectionStatus.value = 'disconnected';
      subscriptionMap.clear();
      subscriptionCallbacks.clear();
      connectPromise = null;
      console.log('[StompClient] 已断开连接');
    } catch (error) {
      console.error('[StompClient] 断开连接失败:', error);
      throw error;
    }
  }
  
  /**
   * 订阅指定主题
   */
  function subscribe(topic: string, callback: MessageCallback): StompSubscription {
    const client = getClient();
    
    if (subscriptionMap.has(topic)) {
      console.warn(`[StompClient] 主题 ${topic} 已订阅,将取消旧订阅`);
      unsubscribe(topic);
    }
    
    subscriptionCallbacks.set(topic, callback);
    
    if (connectionStatus.value === 'connected') {
      const subscription = client.subscribe(topic, (message: IMessage) => {
        let body: any = message.body;
        try {
          body = JSON.parse(message.body);
        } catch {
          // 保持原字符串
        }
        callback(body, message.headers as Record<string, string>);
      });
      
      subscriptionMap.set(topic, subscription);
      console.log(`[StompClient] 已订阅: ${topic}`);
      return subscription;
    } else {
      // 未连接时,先保存回调,连接成功后会通过 onConnect 自动订阅
      console.log(`[StompClient] 已保存订阅回调: ${topic}(等待连接)`);
      return {
        unsubscribe: () => unsubscribe(topic),
      } as StompSubscription;
    }
  }
  
  /**
   * 取消订阅指定主题
   */
  function unsubscribe(topic: string): void {
    if (subscriptionMap.has(topic)) {
      const subscription = subscriptionMap.get(topic);
      subscription?.unsubscribe();
      subscriptionMap.delete(topic);
      console.log(`[StompClient] 已取消订阅: ${topic}`);
    }
    
    subscriptionCallbacks.delete(topic);
  }
  
  /**
   * 向指定目的地发送消息
   */
  function send(destination: string, body: any, headers?: Record<string, string>): void {
    if (connectionStatus.value !== 'connected') {
      throw new Error('未连接,无法发送消息');
    }
    
    const client = getClient();
    const payload = typeof body === 'string' ? body : JSON.stringify(body);
    
    client.publish({
      destination,
      body: payload,
      headers: {
        'content-type': 'application/json',
        ...headers,
      },
    });
  }
  
  /**
   * 检查当前是否已连接
   */
  function isConnected(): boolean {
    return connectionStatus.value === 'connected';
  }
  
  /**
   * 检查是否已订阅指定主题
   */
  function isSubscribed(topic: string): boolean {
    return subscriptionMap.has(topic);
  }
  
  return {
    // 响应式状态
    connectionStatus,
    
    // 连接管理
    connect,
    disconnect,
    
    // 订阅管理
    subscribe,
    unsubscribe,
    
    // 消息发送
    send,
    
    // 状态检查
    isConnected,
    isSubscribed,
  };
}

export type { ConnectionStatus, MessageCallback };

9.2 WebSocket Store (websocket.ts)

// src/stores/websocket.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useWebsocketStore = defineStore('websocket', () => {
  // 连接指定ip
  const wsUrlNew = ref<string>('');
  
  const setWsUrlNew = (url: string) => {
    wsUrlNew.value = url;
    console.log('[WebSocket Store] 地址已更新:', url.length > 0 ? url : '地址为空');
  };
  
  return {
    wsUrlNew,
    setWsUrlNew,
  };
});

9.3 安装依赖

npm install @stomp/stompjs

9.4 使用示例

// 在 Vue 组件中使用
import { useStompClient } from '@/utils/stomp-client';
import { useWebsocketStore } from '@/stores/websocket';

// 设置 WebSocket 地址
const wsStore = useWebsocketStore();
wsStore.setWsUrlNew('ws://localhost:8080/ws');

// 创建客户端
const { connect, subscribe, send, disconnect, connectionStatus } = useStompClient({
  onConnect: () => console.log('连接成功'),
  onDisconnect: () => console.log('连接断开'),
});

// 连接
await connect();

// 订阅
const subscription = subscribe('/topic/notifications', (body, headers) => {
  console.log('收到通知:', body);
});

// 发送消息
send('/app/chat', { text: 'Hello World' });

// 断开连接
await disconnect();

9.5 扩展建议

根据项目需求,可以进一步扩展功能:

  1. 消息队列:在离线时缓存消息,连接恢复后重新发送
  2. 自动重连策略:实现指数退避重连机制
  3. 消息压缩:对大消息进行压缩传输
  4. 认证支持:集成 JWT、OAuth 等认证方式
  5. 连接池:支持多个 WebSocket 连接
  6. 消息持久化:将重要消息保存到本地存储

这个完整的封装提供了 URL 热切换、自动重订阅、响应式状态管理等核心功能,可以直接在项目中使用。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐