feat:RESTful API架构改WebSocket架构-执行结果可以分步显示版本
This commit is contained in:
270
frontend/src/utils/websocket.ts
Normal file
270
frontend/src/utils/websocket.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* WebSocket 客户端封装
|
||||
* 基于 socket.io-client 实现
|
||||
*/
|
||||
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
interface WebSocketConfig {
|
||||
url?: string
|
||||
reconnectionAttempts?: number
|
||||
reconnectionDelay?: number
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface RequestMessage {
|
||||
id: string
|
||||
action: string
|
||||
data: any
|
||||
}
|
||||
|
||||
interface ResponseMessage {
|
||||
id: string
|
||||
status: 'success' | 'error' | 'streaming' | 'complete'
|
||||
data?: any
|
||||
error?: string
|
||||
stage?: string
|
||||
message?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface StreamProgressCallback {
|
||||
(data: any): void
|
||||
}
|
||||
|
||||
type RequestHandler = {
|
||||
resolve: (value: any) => void
|
||||
reject: (error: Error) => void
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
onProgress?: StreamProgressCallback
|
||||
}
|
||||
|
||||
class WebSocketClient {
|
||||
private socket: Socket | null = null
|
||||
private requestHandlers = new Map<string, RequestHandler>()
|
||||
private streamHandlers = new Map<string, StreamProgressCallback>()
|
||||
private config: Required<WebSocketConfig>
|
||||
private isConnected = false
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
url: '',
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
timeout: 300000, // 5分钟超时
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到WebSocket服务器
|
||||
*/
|
||||
connect(url?: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const wsUrl = url || this.config.url || window.location.origin
|
||||
|
||||
this.socket = io(wsUrl, {
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: this.config.reconnectionAttempts,
|
||||
reconnectionDelay: this.config.reconnectionDelay,
|
||||
})
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true
|
||||
resolve()
|
||||
})
|
||||
|
||||
this.socket.on('connect_error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.isConnected = false
|
||||
})
|
||||
|
||||
this.socket.on('connected', (data) => {
|
||||
// Server connected message
|
||||
})
|
||||
|
||||
// 监听响应消息
|
||||
this.socket.on('response', (response: ResponseMessage) => {
|
||||
const { id, status, data, error } = response
|
||||
const handler = this.requestHandlers.get(id)
|
||||
|
||||
if (handler) {
|
||||
// 清除超时定时器
|
||||
if (handler.timer) {
|
||||
clearTimeout(handler.timer)
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
handler.resolve(data)
|
||||
} else {
|
||||
handler.reject(new Error(error || 'Unknown error'))
|
||||
}
|
||||
|
||||
// 删除处理器
|
||||
this.requestHandlers.delete(id)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听流式进度消息
|
||||
this.socket.on('progress', (response: ResponseMessage) => {
|
||||
const { id, status, data, error } = response
|
||||
|
||||
// 首先检查是否有对应的流式处理器
|
||||
const streamCallback = this.streamHandlers.get(id)
|
||||
if (streamCallback) {
|
||||
if (status === 'streaming') {
|
||||
// 解析 data 字段(JSON 字符串)并传递给回调
|
||||
try {
|
||||
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
|
||||
streamCallback(parsedData)
|
||||
} catch (e) {
|
||||
// Failed to parse progress data
|
||||
}
|
||||
} else if (status === 'complete') {
|
||||
this.streamHandlers.delete(id)
|
||||
streamCallback({ type: 'complete' })
|
||||
} else if (status === 'error') {
|
||||
this.streamHandlers.delete(id)
|
||||
streamCallback({ type: 'error', error })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有对应的普通请求处理器(支持send()方法的进度回调)
|
||||
const requestHandler = this.requestHandlers.get(id)
|
||||
if (requestHandler && requestHandler.onProgress) {
|
||||
// 解析 data 字段并传递给进度回调
|
||||
try {
|
||||
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
|
||||
requestHandler.onProgress(parsedData)
|
||||
} catch (e) {
|
||||
// Failed to parse progress data
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 心跳检测
|
||||
this.socket.on('pong', () => {
|
||||
// Pong received
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求(双向通信,支持可选的进度回调)
|
||||
*/
|
||||
send(action: string, data: any, timeout?: number, onProgress?: StreamProgressCallback): Promise<any> {
|
||||
if (!this.socket || !this.isConnected) {
|
||||
return Promise.reject(new Error('WebSocket未连接'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 设置超时
|
||||
const timeoutMs = timeout || this.config.timeout
|
||||
const timer = setTimeout(() => {
|
||||
if (this.requestHandlers.has(requestId)) {
|
||||
this.requestHandlers.delete(requestId)
|
||||
reject(new Error(`Request timeout: ${action}`))
|
||||
}
|
||||
}, timeoutMs)
|
||||
|
||||
// 保存处理器(包含可选的进度回调)
|
||||
this.requestHandlers.set(requestId, { resolve, reject, timer, onProgress })
|
||||
|
||||
// 发送消息
|
||||
this.socket!.emit(action, {
|
||||
id: requestId,
|
||||
action,
|
||||
data,
|
||||
} as RequestMessage)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅流式数据
|
||||
*/
|
||||
subscribe(
|
||||
action: string,
|
||||
data: any,
|
||||
onProgress: StreamProgressCallback,
|
||||
onComplete?: () => void,
|
||||
onError?: (error: Error) => void,
|
||||
): void {
|
||||
if (!this.socket || !this.isConnected) {
|
||||
onError?.(new Error('WebSocket未连接'))
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// 保存流式处理器
|
||||
const wrappedCallback = (progressData: any) => {
|
||||
if (progressData?.type === 'complete') {
|
||||
this.streamHandlers.delete(requestId)
|
||||
onComplete?.()
|
||||
} else {
|
||||
onProgress(progressData)
|
||||
}
|
||||
}
|
||||
|
||||
this.streamHandlers.set(requestId, wrappedCallback)
|
||||
|
||||
// 发送订阅请求
|
||||
this.socket.emit(action, {
|
||||
id: requestId,
|
||||
action,
|
||||
data,
|
||||
} as RequestMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳
|
||||
*/
|
||||
ping(): void {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('ping')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.socket) {
|
||||
// 清理所有处理器
|
||||
this.requestHandlers.forEach((handler) => {
|
||||
if (handler.timer) {
|
||||
clearTimeout(handler.timer)
|
||||
}
|
||||
})
|
||||
this.requestHandlers.clear()
|
||||
this.streamHandlers.clear()
|
||||
|
||||
this.socket.disconnect()
|
||||
this.socket = null
|
||||
this.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
get connected(): boolean {
|
||||
return this.isConnected && this.socket?.connected === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Socket ID
|
||||
*/
|
||||
get id(): string | undefined {
|
||||
return this.socket?.id
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export default new WebSocketClient()
|
||||
Reference in New Issue
Block a user