Files
FastapiAdmin/frontend/web/src/utils/socket/index.ts
T
zhangtao 9a231aa58c feat(web): 升级至 v3.0 并重构组件与引导流程
- 提取应用引导逻辑至 useAppBootstrap 组合式函数
- 增强日期选择器组件,支持全类型与格式自动推导
- 修复资源泄漏:清理 RAF、定时器、事件与 mitt 监听
- 修复退出登录逻辑与异步导出流程
- 迁移 ::v-deep 至 Vue 3 标准 :deep 语法
- 移除调试日志并改进类型定义与注释文档
2026-05-11 23:37:11 +08:00

542 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WebSocket 服务管理
*
* @description
* 统一管理应用中的所有 WebSocket 连接
* - 字典同步 WebSocket
* - 在线用户计数 WebSocket
* - 其他业务 WebSocket
*
* @author fastapiadmin
*/
import { Auth } from "@utils/auth";
/**
* WebSocket 服务实例约定接口
*/
type WebSocketService = {
disconnect?: () => void;
closeWebSocket?: () => void;
cleanup?: () => void;
[key: string]: any;
};
/**
* 全局 WebSocket 实例管理
*/
const websocketInstances = new Map<string, WebSocketService>();
/**
* 防止重复初始化的状态标记
*/
let isInitialized = false;
/**
* 注册 WebSocket 实例
*/
export function registerWebSocketInstance(key: string, instance: WebSocketService) {
websocketInstances.set(key, instance);
}
/**
* 获取 WebSocket 实例
*/
export function getWebSocketInstance(key: string) {
return websocketInstances.get(key);
}
/**
* 初始化 WebSocket 服务
*/
export function setupWebSocket() {
if (isInitialized) {
console.warn("[WebSocket] 已初始化,跳过重复初始化");
return;
}
if (!Auth.getAccessToken()) {
console.warn("[WebSocket] 未登录,跳过 WebSocket 初始化");
return;
}
try {
isInitialized = true;
console.info("[WebSocket] 初始化成功");
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
}
/**
* 清理所有 WebSocket 连接
*/
export function cleanupWebSocket() {
console.info("[WebSocket] 开始清理连接...");
websocketInstances.forEach((instance, key) => {
try {
if (instance.disconnect) {
instance.disconnect();
} else if (instance.closeWebSocket) {
instance.closeWebSocket();
} else if (instance.cleanup) {
instance.cleanup();
}
console.info(`[WebSocket] ${key} 已断开`);
} catch (error) {
console.error(`[WebSocket] ${key} 清理失败:`, error);
}
});
websocketInstances.clear();
isInitialized = false;
console.info("[WebSocket] 清理完成");
}
/**
* 重新初始化 WebSocket
*/
export function reinitializeWebSocket() {
cleanupWebSocket();
setupWebSocket();
}
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => {
cleanupWebSocket();
});
}
/**
* WebSocket 客户端类
*
* 提供 WebSocket 连接管理功能,支持自动重连、心跳检测和消息队列
*
* @module utils/socket
*/
interface WebSocketOptions {
url?: string;
messageHandler: (event: MessageEvent) => void;
reconnectInterval?: number; // 重连间隔(ms)
heartbeatInterval?: number; // 心跳检测间隔(ms)
pingInterval?: number; // 发送ping间隔(ms)
reconnectTimeout?: number; // 重连超时时间(ms)
maxReconnectAttempts?: number; // 最大重连次数
connectionTimeout?: number; // 连接建立超时时间(ms)
}
export default class WebSocketClient {
private static instance: WebSocketClient | null = null;
private ws: WebSocket | null = null;
private url: string;
private messageHandler: (event: MessageEvent) => void;
private reconnectInterval: number;
private heartbeatInterval: number;
private pingInterval: number;
private reconnectTimeout: number;
private maxReconnectAttempts: number;
private connectionTimeout: number;
private reconnectAttempts: number = 0; // 当前重连次数
// 消息队列 - 缓存连接建立前的消息
private messageQueue: Array<string | ArrayBufferLike | Blob | ArrayBufferView> = [];
// 定时器
private detectionTimer: NodeJS.Timeout | null = null;
private timeoutTimer: NodeJS.Timeout | null = null;
private reconnectTimer: NodeJS.Timeout | null = null;
private pingTimer: NodeJS.Timeout | null = null;
private connectionTimer: NodeJS.Timeout | null = null; // 连接超时定时器
// 状态标识
private isConnected: boolean = false;
private isConnecting: boolean = false; // 是否正在连接中
private stopReconnect: boolean = false;
private isReconnecting: boolean = false;
private constructor(options: WebSocketOptions) {
this.url = options.url || (process.env.VUE_APP_LOGIN_WEBSOCKET as string);
this.messageHandler = options.messageHandler;
this.reconnectInterval = options.reconnectInterval ?? 20 * 1000; // 默认20秒
this.heartbeatInterval = options.heartbeatInterval ?? 5 * 1000; // 默认5秒
this.pingInterval = options.pingInterval ?? 10 * 1000; // 默认10秒
this.reconnectTimeout = options.reconnectTimeout ?? 30 * 1000; // 默认30秒
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10; // 默认最多重连10次
this.connectionTimeout = options.connectionTimeout ?? 10 * 1000; // 连接超时10秒
}
// 单例模式获取实例
static getInstance(options: WebSocketOptions): WebSocketClient {
if (!WebSocketClient.instance) {
WebSocketClient.instance = new WebSocketClient(options);
} else {
// 更新消息处理器
WebSocketClient.instance.messageHandler = options.messageHandler;
// 如果提供了新的URL,则更新并重新连接
if (options.url && WebSocketClient.instance.url !== options.url) {
WebSocketClient.instance.url = options.url;
WebSocketClient.instance.reconnectAttempts = 0;
WebSocketClient.instance.init();
}
}
return WebSocketClient.instance;
}
// 初始化连接
init(): void {
this.connect(true);
}
private connect(resetReconnectAttempts: boolean = false): void {
// 如果正在连接中,不重复连接
if (this.isConnecting) {
console.info("正在建立WebSocket连接中...");
return;
}
// 如果已连接,不重复连接
if (this.ws?.readyState === WebSocket.OPEN) {
console.warn("WebSocket连接已存在");
this.flushMessageQueue(); // 确保队列中的消息被发送
return;
}
try {
this.isConnecting = true;
this.stopReconnect = false;
if (resetReconnectAttempts) {
this.reconnectAttempts = 0;
this.isReconnecting = false;
this.clearTimer("reconnectTimer");
}
this.ws = new WebSocket(this.url);
// 设置连接超时检测
this.clearTimer("connectionTimer");
this.connectionTimer = setTimeout(() => {
console.error(`WebSocket连接超时 (${this.connectionTimeout}ms)${this.url}`);
this.handleConnectionTimeout();
}, this.connectionTimeout);
this.ws.onopen = (event) => this.handleOpen(event);
this.ws.onmessage = (event) => this.handleMessage(event);
this.ws.onclose = (event) => this.handleClose(event);
this.ws.onerror = (event) => this.handleError(event);
} catch (error) {
console.error("WebSocket初始化失败:", error);
this.isConnecting = false;
this.reconnect();
}
}
// 处理连接超时
private handleConnectionTimeout(): void {
if (this.ws?.readyState !== WebSocket.OPEN) {
console.error("WebSocket连接超时,强制关闭连接");
this.ws?.close(1000, "Connection timeout");
this.isConnecting = false;
this.reconnect();
}
}
// 关闭连接
close(force?: boolean): void {
this.clearAllTimers();
this.stopReconnect = true;
this.isReconnecting = false;
this.isConnecting = false;
if (this.ws) {
// 1000 表示正常关闭
this.ws.close(force ? 1001 : 1000, force ? "Force closed" : "Normal close");
this.ws = null;
}
this.isConnected = false;
}
// 发送消息 - 增加消息队列
send(data: string | ArrayBufferLike | Blob | ArrayBufferView, immediate: boolean = false): void {
// 如果要求立即发送且未连接,则直接报错
if (immediate && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
console.error("WebSocket未连接,无法立即发送消息");
return;
}
// 如果未连接且不要求立即发送,则加入消息队列
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.info("WebSocket未连接,消息已加入队列等待发送");
this.messageQueue.push(data);
// 如果未在重连中,则尝试重连
if (!this.isConnecting && !this.stopReconnect) {
this.init();
}
return;
}
try {
this.ws.send(data);
} catch (error) {
console.error("WebSocket发送消息失败:", error);
// 发送失败时将消息加入队列,等待重连后重试
this.messageQueue.push(data);
this.reconnect();
}
}
// 发送队列中的消息
private flushMessageQueue(): void {
if (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
console.info(`发送队列中的${this.messageQueue.length}条消息`);
while (this.messageQueue.length > 0) {
const data = this.messageQueue.shift();
if (data) {
try {
this.ws?.send(data);
} catch (error) {
console.error("发送队列消息失败:", error);
// 如果发送失败,将消息放回队列头部
if (data) this.messageQueue.unshift(data);
break;
}
}
}
}
}
// 处理连接打开
private handleOpen(event: Event): void {
console.info("WebSocket连接成功", event);
this.clearTimer("connectionTimer"); // 清除连接超时定时器
this.isConnected = true;
this.isConnecting = false;
this.isReconnecting = false;
this.stopReconnect = false;
this.reconnectAttempts = 0; // 重置重连次数
this.startHeartbeat();
this.startPing();
this.flushMessageQueue(); // 发送队列中的消息
}
// 处理收到的消息
private handleMessage(event: MessageEvent): void {
console.debug("收到WebSocket消息:", event);
this.resetHeartbeat();
this.messageHandler(event);
}
// 处理连接关闭
private handleClose(event: CloseEvent): void {
console.info(
`WebSocket断开: 代码=${event.code}, 原因=${event.reason}, 干净关闭=${event.wasClean}`
);
// 1000 是正常关闭代码
const isNormalClose = event.code === 1000;
this.isConnected = false;
this.isConnecting = false;
this.clearConnectionTimers();
this.ws = null;
if (!this.stopReconnect && !isNormalClose) {
this.reconnect();
}
}
// 处理错误 - 增加详细错误信息
private handleError(event: Event): void {
console.error("WebSocket连接错误:");
console.error("错误事件:", event);
console.error(
"当前连接状态:",
this.ws?.readyState ? this.getReadyStateText(this.ws.readyState) : "未初始化"
);
this.isConnected = false;
this.isConnecting = false;
// 只有在未停止重连的情况下才尝试重连
if (!this.stopReconnect) {
this.reconnect();
}
}
private closeCurrentSocketForReconnect(): void {
this.clearConnectionTimers();
this.isConnected = false;
this.isConnecting = false;
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onclose = null;
this.ws.onerror = null;
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
this.ws.close(1001, "Reconnect");
}
this.ws = null;
}
}
// 转换连接状态为文本描述
private getReadyStateText(state: number): string {
switch (state) {
case WebSocket.CONNECTING:
return "CONNECTING (0) - 正在连接";
case WebSocket.OPEN:
return "OPEN (1) - 已连接";
case WebSocket.CLOSING:
return "CLOSING (2) - 正在关闭";
case WebSocket.CLOSED:
return "CLOSED (3) - 已关闭";
default:
return `未知状态 (${state})`;
}
}
// 开始心跳检测
private startHeartbeat(): void {
this.clearTimer("detectionTimer");
this.clearTimer("timeoutTimer");
this.detectionTimer = setTimeout(() => {
this.isConnected = this.ws?.readyState === WebSocket.OPEN;
if (!this.isConnected) {
console.warn("WebSocket心跳检测失败,尝试重连");
this.reconnect();
this.timeoutTimer = setTimeout(() => {
console.warn("WebSocket重连超时");
this.close();
}, this.reconnectTimeout);
}
}, this.heartbeatInterval);
}
// 重置心跳检测
private resetHeartbeat(): void {
this.clearTimer("detectionTimer");
this.clearTimer("timeoutTimer");
this.startHeartbeat();
}
// 开始发送ping消息
private startPing(): void {
this.clearTimer("pingTimer");
this.pingTimer = setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
console.warn("WebSocket未连接,停止发送ping");
this.clearTimer("pingTimer");
this.reconnect();
return;
}
try {
this.ws.send("ping");
console.debug("发送ping消息");
} catch (error) {
console.error("发送ping消息失败:", error);
this.clearTimer("pingTimer");
this.reconnect();
}
}, this.pingInterval);
}
// 重连 - 增加重连次数限制
private reconnect(): void {
if (this.stopReconnect || this.isConnecting || this.reconnectInterval <= 0) {
return;
}
// 检查是否超过最大重连次数
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`);
this.close(true);
return;
}
this.reconnectAttempts++;
this.isReconnecting = true;
this.closeCurrentSocketForReconnect();
const delay = this.calculateReconnectDelay();
console.info(
`将在${delay / 1000}秒后尝试重新连接(第${this.reconnectAttempts}/${this.maxReconnectAttempts}次)`
);
this.clearTimer("reconnectTimer");
this.reconnectTimer = setTimeout(() => {
console.info(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`);
this.connect(false);
}, delay);
}
// 计算重连延迟 - 指数退避策略
private calculateReconnectDelay(): number {
// 基础延迟 + 随机值,避免多个客户端同时重连
const jitter = Math.random() * 1000; // 0-1秒的随机延迟
const baseDelay = Math.min(
this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
this.reconnectInterval * 5
);
return baseDelay + jitter;
}
// 清除指定定时器
private clearTimer(
timerName:
| "detectionTimer"
| "timeoutTimer"
| "reconnectTimer"
| "pingTimer"
| "connectionTimer"
): void {
if (this[timerName]) {
clearTimeout(this[timerName] as NodeJS.Timeout);
this[timerName] = null;
}
}
// 清除所有定时器
private clearAllTimers(): void {
this.clearConnectionTimers();
this.clearTimer("reconnectTimer");
}
private clearConnectionTimers(): void {
this.clearTimer("detectionTimer");
this.clearTimer("timeoutTimer");
this.clearTimer("pingTimer");
this.clearTimer("connectionTimer");
}
// 获取当前连接状态
get isWebSocketConnected(): boolean {
return this.isConnected;
}
// 获取当前连接状态文本
get connectionStatusText(): string {
if (this.isConnecting) return "正在连接";
if (this.isConnected) return "已连接";
if (this.isReconnecting && this.reconnectAttempts > 0)
return `重连中(${this.reconnectAttempts}/${this.maxReconnectAttempts}`;
return "已断开";
}
// 销毁实例
static destroyInstance(): void {
if (WebSocketClient.instance) {
WebSocketClient.instance.close();
WebSocketClient.instance = null;
}
}
}