refactor: 项目重构

This commit is contained in:
Ray.Hao
2025-05-24 07:35:46 +08:00
parent cfe041d7d2
commit 32686ad807
51 changed files with 1201 additions and 696 deletions

View File

@@ -1,6 +1,6 @@
import { useDictStoreHook } from "@/store/modules/dict.store";
import { useStomp } from "./useStomp";
import { IMessage } from "@stomp/stompjs";
import type { IMessage } from "@stomp/stompjs";
import { ref } from "vue";
// 字典消息类型
@@ -39,6 +39,9 @@ function createDictSyncHook() {
// 消息回调函数列表
const messageCallbacks = ref<DictMessageCallback[]>([]);
// 重试定时器
let retryTimer: any = null;
/**
* 注册字典消息回调
* @param callback 回调函数
@@ -62,7 +65,7 @@ function createDictSyncHook() {
// 检查是否配置了WebSocket端点
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
if (!wsEndpoint) {
console.log("[WebSocket] 未配置WebSocket端点,跳过连接");
console.log("[DictSync] 未配置WebSocket端点,跳过连接");
return;
}
@@ -72,7 +75,7 @@ function createDictSyncHook() {
// 设置字典订阅
setupDictSubscription();
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
console.error("[DictSync] 初始化失败:", error);
}
};
@@ -80,6 +83,12 @@ function createDictSyncHook() {
* 关闭WebSocket
*/
const closeWebSocket = () => {
// 清理重试定时器
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
// 取消所有订阅
subscriptionIds.value.forEach((id) => {
unsubscribe(id);
@@ -99,27 +108,40 @@ function createDictSyncHook() {
// 防止重复订阅
if (subscribedTopics.value.has(topic)) {
console.log(`跳过重复订阅: ${topic}`);
console.log(`[DictSync] 跳过重复订阅: ${topic}`);
return;
}
console.log(`开始尝试订阅字典主题: ${topic}`);
console.log(`[DictSync] 开始尝试订阅字典主题: ${topic}`);
// 使用简化的重试逻辑依赖useStomp的连接管理
const attemptSubscribe = () => {
if (!isConnected.value) {
console.log("等待WebSocket连接建立...");
console.log("[DictSync] 等待WebSocket连接建立...");
// 清理之前的定时器,防止重复
if (retryTimer) {
clearTimeout(retryTimer);
}
// 10秒后再次尝试
setTimeout(attemptSubscribe, 10000);
retryTimer = setTimeout(() => {
retryTimer = null;
attemptSubscribe();
}, 10000);
return;
}
// 清理重试定时器
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
// 检查是否已订阅
if (subscribedTopics.value.has(topic)) {
return;
}
console.log(`连接已建立,开始订阅: ${topic}`);
console.log(`[DictSync] 连接已建立,开始订阅: ${topic}`);
// 订阅字典更新
const subId = subscribe(topic, (message: IMessage) => {
@@ -129,9 +151,9 @@ function createDictSyncHook() {
if (subId) {
subscriptionIds.value.push(subId);
subscribedTopics.value.add(topic);
console.log(`字典主题订阅成功: ${topic}`);
console.log(`[DictSync] 字典主题订阅成功: ${topic}`);
} else {
console.warn(`字典主题订阅失败: ${topic}`);
console.warn(`[DictSync] 字典主题订阅失败: ${topic}`);
}
};
@@ -148,7 +170,7 @@ function createDictSyncHook() {
try {
// 记录接收到的消息
console.log(`收到字典更新消息: ${message.body}`);
console.log(`[DictSync] 收到字典更新消息: ${message.body}`);
// 尝试解析消息
const parsedData = JSON.parse(message.body) as DictMessage;
@@ -158,21 +180,21 @@ function createDictSyncHook() {
// 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode);
console.log(`字典缓存已清除: ${dictCode}`);
console.log(`[DictSync] 字典缓存已清除: ${dictCode}`);
// 调用所有注册的回调函数
messageCallbacks.value.forEach((callback) => {
try {
callback(parsedData);
} catch (callbackError) {
console.error("[WebSocket] 回调执行失败:", callbackError);
console.error("[DictSync] 回调执行失败:", callbackError);
}
});
// 显示提示消息
console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`);
console.info(`[DictSync] 字典 ${dictCode} 已变更,将在下次使用时自动加载`);
} catch (error) {
console.error("[WebSocket] 解析消息失败:", error);
console.error("[DictSync] 解析消息失败:", error);
}
};

View File

@@ -1,14 +1,16 @@
import { ref, onMounted, onUnmounted, watch } from "vue";
import { ref, onMounted, onUnmounted, watch, getCurrentInstance } from "vue";
import { useStomp } from "./useStomp";
import { ElMessage } from "element-plus";
import { Storage } from "@/utils/storage";
import { ACCESS_TOKEN_KEY } from "@/constants/cache-keys";
import { registerWebSocketInstance } from "@/plugins/websocket";
import { Auth } from "@/utils/auth";
// 全局单例实例
let globalInstance: ReturnType<typeof createOnlineCountHook> | null = null;
/**
* 在线用户计数组合式函数
* 用于订阅后端推送的在线用户数量变化
* 创建在线用户计数的核心逻辑
*/
export function useOnlineCount() {
function createOnlineCountHook() {
// 在线用户数量
const onlineUserCount = ref(0);
@@ -22,18 +24,23 @@ export function useOnlineCount() {
const isConnecting = ref(false);
// 使用Stomp客户端 - 配置使用指数退避策略
const stompInstance = useStomp({
reconnectDelay: 15000, // 重连基础延迟
maxReconnectAttempts: 3, // 重连次数上限
connectionTimeout: 10000, // 连接超时
useExponentialBackoff: true, // 启用指数退避
});
const {
connect,
subscribe,
unsubscribe,
disconnect,
isConnected: stompConnected,
} = useStomp({
reconnectDelay: 15000, // 重连基础延迟
maxReconnectAttempts: 3, // 重连次数上限
connectionTimeout: 10000, // 连接超时
useExponentialBackoff: true, // 启用指数退避
});
} = stompInstance;
// 注册到全局实例管理器
registerWebSocketInstance("onlineCount", stompInstance);
// 订阅ID
let subscriptionId = "";
@@ -49,7 +56,7 @@ export function useOnlineCount() {
// 一旦连接成功,立即订阅主题
subscribeToOnlineCount();
console.log("WebSocket连接成功已订阅在线用户计数主题");
console.log("[useOnlineCount] WebSocket连接成功已订阅在线用户计数主题");
}
});
@@ -81,7 +88,7 @@ export function useOnlineCount() {
lastUpdateTime.value = Date.now();
}
} catch (error) {
console.error("解析在线用户数量失败:", error);
console.error("[useOnlineCount] 解析在线用户数量失败:", error);
}
});
};
@@ -95,18 +102,19 @@ export function useOnlineCount() {
// 检查WebSocket端点是否配置
const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
if (!wsEndpoint) {
console.log("未配置WebSocket端点(VITE_APP_WS_ENDPOINT),跳过WebSocket连接");
console.log("[useOnlineCount] 未配置WebSocket端点(VITE_APP_WS_ENDPOINT),跳过WebSocket连接");
return;
}
// 检查是否有可用的令牌
const hasToken = !!Storage.get(ACCESS_TOKEN_KEY, "");
if (!hasToken) {
console.log("没有检测到有效令牌不尝试WebSocket连接");
// 使用 Auth.getAccessToken() 获取令牌,确保获取到最新的
const accessToken = Auth.getAccessToken();
if (!accessToken) {
console.log("[useOnlineCount] 没有检测到有效令牌不尝试WebSocket连接");
return;
}
isConnecting.value = true;
console.log("[useOnlineCount] 开始建立WebSocket连接...");
// 连接WebSocket
connect();
@@ -115,17 +123,17 @@ export function useOnlineCount() {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
console.warn("WebSocket连接超时将自动尝试重连");
console.warn("[useOnlineCount] WebSocket连接超时将自动尝试重连");
ElMessage.warning("正在尝试连接服务器,请稍候...");
// 超时后尝试重新连接
closeWebSocket();
setTimeout(() => {
// 再次检查令牌有效性
if (Storage.get(ACCESS_TOKEN_KEY, "")) {
if (Auth.getAccessToken()) {
initWebSocket();
} else {
console.log("令牌无效,放弃重连");
console.log("[useOnlineCount] 令牌无效,放弃重连");
}
}, 3000);
}
@@ -160,16 +168,6 @@ export function useOnlineCount() {
isConnecting.value = false;
};
// 组件挂载时初始化WebSocket
onMounted(() => {
initWebSocket();
});
// 组件卸载时关闭WebSocket
onUnmounted(() => {
closeWebSocket();
});
return {
onlineUserCount,
lastUpdateTime,
@@ -179,3 +177,39 @@ export function useOnlineCount() {
closeWebSocket,
};
}
/**
* 在线用户计数组合式函数
* 使用单例模式,避免重复创建 WebSocket 连接
* @param options 配置选项
* @param options.autoInit 是否在组件挂载时自动初始化(默认 true
*/
export function useOnlineCount(options: { autoInit?: boolean } = {}) {
const { autoInit = true } = options;
if (!globalInstance) {
globalInstance = createOnlineCountHook();
}
// 只有在组件上下文中且 autoInit 为 true 时才使用生命周期钩子
if (autoInit && getCurrentInstance()) {
// 组件挂载时检查是否需要初始化WebSocket
onMounted(() => {
// 只有在未连接且未连接中时才尝试初始化
if (!globalInstance!.isConnected.value && !globalInstance!.isConnecting.value) {
console.log("[useOnlineCount] 组件挂载尝试初始化WebSocket连接");
globalInstance!.initWebSocket();
} else {
console.log("[useOnlineCount] WebSocket已连接或正在连接跳过初始化");
}
});
// 组件卸载时不关闭连接,保持全局连接
onUnmounted(() => {
// 不关闭连接,让其他组件继续使用
console.log("[useOnlineCount] Component unmounted, keeping WebSocket connection");
});
}
return globalInstance;
}

View File

@@ -1,4 +1,4 @@
import { Client, IMessage, StompSubscription } from "@stomp/stompjs";
import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs";
import { Auth } from "@/utils/auth";
export interface UseStompOptions {
@@ -48,13 +48,18 @@ export function useStomp(options: UseStompOptions = {}) {
const subscriptions = new Map<string, StompSubscription>();
// 用于保存 STOMP 客户端的实例
let client = ref<Client | null>(null);
const client = ref<Client | null>(null);
// 防止重复连接的标志
let isConnecting = false;
let isManualDisconnect = false;
/**
* 初始化 STOMP 客户端
*/
const initializeClient = () => {
if (client.value) {
// 如果客户端已存在且正在连接或已连接,直接返回
if (client.value && (client.value.active || client.value.connected)) {
console.log("STOMP客户端已存在且处于活动状态跳过初始化");
return;
}
@@ -73,6 +78,16 @@ export function useStomp(options: UseStompOptions = {}) {
return;
}
// 如果有旧的客户端,先清理
if (client.value) {
try {
client.value.deactivate();
} catch (error) {
console.warn("清理旧客户端时出错:", error);
}
client.value = null;
}
// 创建 STOMP 客户端
client.value = new Client({
brokerURL: brokerURL.value,
@@ -80,7 +95,7 @@ export function useStomp(options: UseStompOptions = {}) {
Authorization: `Bearer ${currentToken}`,
},
debug: options.debug ? console.log : () => {},
reconnectDelay: useExponentialBackoff ? 0 : reconnectDelay, // 禁用内置重连机制
reconnectDelay: 0, // 禁用内置重连机制,使用自定义重连
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
@@ -88,18 +103,21 @@ export function useStomp(options: UseStompOptions = {}) {
// 设置连接监听器
client.value.onConnect = () => {
isConnected.value = true;
isConnecting = false;
reconnectCount.value = 0;
clearTimeout(connectionTimeoutTimer);
clearTimeout(reconnectTimer);
console.log("WebSocket连接已建立");
};
// 设置断开连接监听器
client.value.onDisconnect = () => {
isConnected.value = false;
isConnecting = false;
console.log("WebSocket连接已断开");
// 如果使用自定义指数退避重连策略,则在这里处理
if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) {
// 如果不是手动断开且未达到最大重连次数,则尝试重连
if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) {
handleReconnect();
}
};
@@ -107,32 +125,31 @@ export function useStomp(options: UseStompOptions = {}) {
// 设置 Web Socket 关闭监听器
client.value.onWebSocketClose = (event) => {
isConnected.value = false;
isConnecting = false;
console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`);
// 如果是授权问题导致的关闭,尝试重新获取令牌
if (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) {
console.log("可能是授权问题导致连接关闭,尝试重新建立连接");
// 如果是手动断开,不要重连
if (isManualDisconnect) {
console.log("手动断开连接,不进行重连");
return;
}
// 等待一段时间后再尝试重连,避免立即重连
setTimeout(() => {
// 强制重新初始化客户端,获取最新令牌
client.value = null;
// 如果是授权问题导致的关闭,尝试重连
if (
(event?.code === 1000 || event?.code === 1006 || event?.code === 1008) &&
reconnectCount.value < maxReconnectAttempts
) {
console.log("检测到连接异常关闭,将尝试重连");
// 检查当前是否有有效令牌
const freshToken = Auth.getAccessToken();
if (freshToken) {
initializeClient();
connect();
} else {
console.warn("没有有效令牌暂不重连WebSocket");
}
}, 3000);
// 通过 handleReconnect 统一处理重连,避免重复计数
handleReconnect();
}
};
// 设置错误监听器
client.value.onStompError = (frame) => {
console.error("STOMP错误:", frame.headers, frame.body);
isConnecting = false;
// 检查是否是授权错误
if (
@@ -141,6 +158,8 @@ export function useStomp(options: UseStompOptions = {}) {
frame.body?.includes("Token")
) {
console.warn("WebSocket授权错误请检查登录状态");
// 授权错误不进行重连
isManualDisconnect = true;
}
};
};
@@ -149,13 +168,18 @@ export function useStomp(options: UseStompOptions = {}) {
* 处理重连逻辑
*/
const handleReconnect = () => {
// 如果已经在连接中或手动断开,不重连
if (isConnecting || isManualDisconnect) {
return;
}
if (reconnectCount.value >= maxReconnectAttempts) {
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
return;
}
reconnectCount.value++;
console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
console.log(`准备重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
// 使用指数退避策略增加重连间隔
const delay = useExponentialBackoff
@@ -169,8 +193,9 @@ export function useStomp(options: UseStompOptions = {}) {
// 设置重连计时器
reconnectTimer = setTimeout(() => {
if (!isConnected.value && client.value) {
client.value.activate();
if (!isConnected.value && !isManualDisconnect && !isConnecting) {
console.log(`开始重连...`);
connect();
}
}, delay);
};
@@ -195,12 +220,21 @@ export function useStomp(options: UseStompOptions = {}) {
* 激活连接(如果已经连接或正在激活则直接返回)
*/
const connect = () => {
// 重置手动断开标志
isManualDisconnect = false;
// 检查是否有配置WebSocket端点
if (!brokerURL.value) {
console.error("WebSocket连接失败: 未配置WebSocket端点URL");
return;
}
// 防止重复连接
if (isConnecting) {
console.log("WebSocket正在连接中跳过重复连接请求");
return;
}
if (!client.value) {
initializeClient();
}
@@ -210,29 +244,35 @@ export function useStomp(options: UseStompOptions = {}) {
return;
}
// 避免重复连接:检查是否已连接或正在连接
// 避免重复连接:检查是否已连接
if (client.value.connected) {
console.log("WebSocket已经连接,跳过重复连接");
isConnected.value = true;
return;
}
if (client.value.active) {
console.log("WebSocket连接正在进行中,跳过重复连接请求");
return;
}
// 设置连接标志
isConnecting = true;
// 设置连接超时
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
if (!isConnected.value && isConnecting) {
console.warn("WebSocket连接超时");
if (useExponentialBackoff) {
isConnecting = false;
if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) {
handleReconnect();
}
}
}, connectionTimeout);
client.value.activate();
try {
client.value.activate();
console.log("正在建立WebSocket连接...");
} catch (error) {
console.error("激活WebSocket连接失败:", error);
isConnecting = false;
}
};
/**
@@ -276,31 +316,45 @@ export function useStomp(options: UseStompOptions = {}) {
* 断开WebSocket连接
*/
const disconnect = () => {
if (client.value && client.value.connected) {
// 清除所有订阅
for (const [id, subscription] of subscriptions.entries()) {
subscription.unsubscribe();
subscriptions.delete(id);
}
// 设置手动断开标志
isManualDisconnect = true;
// 断开连接
client.value.deactivate();
console.log("WebSocket连接已断开");
}
// 清除重连计时器
// 清除所有计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
// 清除连接超时计时器
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
connectionTimeoutTimer = null;
}
// 清除所有订阅
for (const [id, subscription] of subscriptions.entries()) {
try {
subscription.unsubscribe();
} catch (error) {
console.warn(`取消订阅 ${id} 时出错:`, error);
}
}
subscriptions.clear();
// 断开连接
if (client.value) {
try {
if (client.value.connected || client.value.active) {
client.value.deactivate();
console.log("WebSocket连接已主动断开");
}
} catch (error) {
console.error("断开WebSocket连接时出错:", error);
}
client.value = null;
}
isConnected.value = false;
isConnecting = false;
reconnectCount.value = 0;
};