Files
1818web-hoduan/WebSocket任务通知接收示例.md
2025-11-14 17:41:15 +08:00

790 lines
20 KiB
Markdown
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 任务通知接收示例
## 一、WebSocket 配置说明
### 后端配置
- **连接端点**: `/user/websocket`(支持 SockJS 备用方案)
- **用户前缀**: `/user`
- **订阅目的地**: `/user/queue/tasks-progress`
- **协议**: STOMP over WebSocket
### 消息格式TaskProgressDto
```typescript
interface TaskProgressDto {
taskNo: string; // 任务编号
status: string; // 任务状态: created/queued/processing/completed/failed
progress: number; // 进度百分比 0-100
message: string; // 进度消息
resultUrl?: string; // 结果URL完成时
errorMessage?: string; // 错误信息(失败时)
}
```
---
## 二、前端依赖安装
### 使用 npm
```bash
npm install @stomp/stompjs sockjs-client
```
### 使用 yarn
```bash
yarn add @stomp/stompjs sockjs-client
```
### CDN 引入HTML
```html
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
```
---
## 三、基础连接示例
### 1. 原生 JavaScript + STOMP.js
```javascript
// 引入依赖(如果使用模块化)
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';
// WebSocket 配置
const WEBSOCKET_URL = 'http://localhost:8081/ws';
const AUTH_TOKEN = 'YOUR_JWT_TOKEN';
// 创建 STOMP 客户端
const stompClient = new Client({
// 使用 SockJS 作为 WebSocket 实现
webSocketFactory: () => new SockJS(WEBSOCKET_URL),
// 连接头(携带认证信息)
connectHeaders: {
Authorization: `Bearer ${AUTH_TOKEN}`
},
// 连接成功回调
onConnect: (frame) => {
console.log('WebSocket 连接成功:', frame);
// 订阅任务进度更新
stompClient.subscribe('/user/queue/tasks-progress', (message) => {
const notification = JSON.parse(message.body);
console.log('收到任务通知:', notification);
// 处理通知
handleTaskNotification(notification);
});
},
// 连接失败回调
onStompError: (frame) => {
console.error('STOMP 错误:', frame);
},
// WebSocket 错误回调
onWebSocketError: (error) => {
console.error('WebSocket 错误:', error);
},
// WebSocket 关闭回调
onWebSocketClose: (event) => {
console.log('WebSocket 连接关闭:', event);
},
// 自动重连配置
reconnectDelay: 5000, // 5秒后重连
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
// 激活连接
stompClient.activate();
// 处理任务通知
function handleTaskNotification(notification) {
const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
switch(status) {
case 'created':
console.log(`[${taskNo}] 任务已创建`);
break;
case 'queued':
console.log(`[${taskNo}] 任务排队中: ${message}`);
break;
case 'processing':
console.log(`[${taskNo}] 处理中 ${progress}%: ${message}`);
updateProgressBar(taskNo, progress);
break;
case 'completed':
console.log(`[${taskNo}] 任务完成: ${resultUrl}`);
showCompletedNotification(taskNo, resultUrl);
break;
case 'failed':
console.error(`[${taskNo}] 任务失败: ${errorMessage || message}`);
showErrorNotification(taskNo, errorMessage || message);
break;
}
}
// 断开连接
function disconnect() {
if (stompClient.connected) {
stompClient.deactivate();
console.log('WebSocket 已断开');
}
}
```
---
## 四、完整封装类(推荐)
### TaskWebSocketClient.js
```javascript
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';
class TaskWebSocketClient {
constructor(baseUrl = 'http://localhost:8081', token) {
this.baseUrl = baseUrl;
this.token = token;
this.client = null;
this.listeners = {
onTaskUpdate: [],
onConnect: [],
onDisconnect: [],
onError: []
};
}
/**
* 连接 WebSocket
*/
connect() {
if (this.client && this.client.connected) {
console.warn('WebSocket 已连接');
return Promise.resolve();
}
return new Promise((resolve, reject) => {
this.client = new Client({
webSocketFactory: () => new SockJS(`${this.baseUrl}/ws`),
connectHeaders: {
Authorization: `Bearer ${this.token}`
},
onConnect: (frame) => {
console.log('✅ WebSocket 连接成功');
// 订阅任务进度更新
this.client.subscribe('/user/queue/tasks-progress', (message) => {
try {
const notification = JSON.parse(message.body);
this._notifyListeners('onTaskUpdate', notification);
} catch (error) {
console.error('解析消息失败:', error);
}
});
this._notifyListeners('onConnect', frame);
resolve(frame);
},
onStompError: (frame) => {
console.error('❌ STOMP 错误:', frame);
this._notifyListeners('onError', frame);
reject(frame);
},
onWebSocketError: (error) => {
console.error('❌ WebSocket 错误:', error);
this._notifyListeners('onError', error);
},
onWebSocketClose: (event) => {
console.log('🔌 WebSocket 连接关闭');
this._notifyListeners('onDisconnect', event);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
this.client.activate();
});
}
/**
* 断开 WebSocket
*/
disconnect() {
if (this.client && this.client.connected) {
this.client.deactivate();
console.log('WebSocket 已断开');
}
}
/**
* 添加任务更新监听器
*/
onTaskUpdate(callback) {
this.listeners.onTaskUpdate.push(callback);
return () => this._removeListener('onTaskUpdate', callback);
}
/**
* 添加连接监听器
*/
onConnect(callback) {
this.listeners.onConnect.push(callback);
return () => this._removeListener('onConnect', callback);
}
/**
* 添加断开连接监听器
*/
onDisconnect(callback) {
this.listeners.onDisconnect.push(callback);
return () => this._removeListener('onDisconnect', callback);
}
/**
* 添加错误监听器
*/
onError(callback) {
this.listeners.onError.push(callback);
return () => this._removeListener('onError', callback);
}
/**
* 通知所有监听器
*/
_notifyListeners(event, data) {
this.listeners[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`监听器执行错误 (${event}):`, error);
}
});
}
/**
* 移除监听器
*/
_removeListener(event, callback) {
const index = this.listeners[event].indexOf(callback);
if (index > -1) {
this.listeners[event].splice(index, 1);
}
}
/**
* 检查连接状态
*/
isConnected() {
return this.client && this.client.connected;
}
}
export default TaskWebSocketClient;
```
---
## 五、使用示例
### 1. React 组件示例
```jsx
import React, { useEffect, useState } from 'react';
import TaskWebSocketClient from './TaskWebSocketClient';
function TaskMonitor() {
const [tasks, setTasks] = useState({});
const [wsClient, setWsClient] = useState(null);
useEffect(() => {
// 获取 Token从 localStorage 或其他地方)
const token = localStorage.getItem('jwt_token');
// 创建 WebSocket 客户端
const client = new TaskWebSocketClient('http://localhost:8081', token);
// 监听任务更新
const unsubscribe = client.onTaskUpdate((notification) => {
console.log('任务更新:', notification);
// 更新任务状态
setTasks(prev => ({
...prev,
[notification.taskNo]: notification
}));
// 根据状态显示不同提示
if (notification.status === 'completed') {
showSuccessToast(`任务 ${notification.taskNo} 已完成!`);
} else if (notification.status === 'failed') {
showErrorToast(`任务 ${notification.taskNo} 失败: ${notification.errorMessage}`);
}
});
// 连接 WebSocket
client.connect().catch(error => {
console.error('连接失败:', error);
});
setWsClient(client);
// 清理函数
return () => {
unsubscribe();
client.disconnect();
};
}, []);
return (
<div>
<h2>任务监控</h2>
{Object.values(tasks).map(task => (
<div key={task.taskNo} className="task-card">
<h3>{task.taskNo}</h3>
<p>状态: {task.status}</p>
<p>进度: {task.progress}%</p>
<p>{task.message}</p>
{task.resultUrl && (
<a href={task.resultUrl} target="_blank" rel="noopener noreferrer">
查看结果
</a>
)}
</div>
))}
</div>
);
}
export default TaskMonitor;
```
### 2. Vue 3 组件示例
```vue
<template>
<div class="task-monitor">
<h2>任务监控</h2>
<div v-for="task in taskList" :key="task.taskNo" class="task-card">
<h3>{{ task.taskNo }}</h3>
<p>状态: {{ task.status }}</p>
<div v-if="task.status === 'processing'">
<progress :value="task.progress" max="100"></progress>
<span>{{ task.progress }}%</span>
</div>
<p>{{ task.message }}</p>
<a v-if="task.resultUrl" :href="task.resultUrl" target="_blank">
查看结果
</a>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import TaskWebSocketClient from './TaskWebSocketClient';
const tasks = ref({});
const taskList = computed(() => Object.values(tasks.value));
let wsClient = null;
onMounted(() => {
const token = localStorage.getItem('jwt_token');
wsClient = new TaskWebSocketClient('http://localhost:8081', token);
// 监听任务更新
wsClient.onTaskUpdate((notification) => {
tasks.value[notification.taskNo] = notification;
if (notification.status === 'completed') {
ElMessage.success(`任务 ${notification.taskNo} 已完成!`);
} else if (notification.status === 'failed') {
ElMessage.error(`任务失败: ${notification.errorMessage}`);
}
});
// 连接
wsClient.connect();
});
onUnmounted(() => {
if (wsClient) {
wsClient.disconnect();
}
});
</script>
```
### 3. 原生 JavaScript 完整示例
```html
<!DOCTYPE html>
<html>
<head>
<title>任务监控</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
</head>
<body>
<h1>AI 任务实时监控</h1>
<div id="connection-status">未连接</div>
<div id="tasks-container"></div>
<script>
const AUTH_TOKEN = localStorage.getItem('jwt_token');
const tasks = {};
// 创建 STOMP 客户端
const client = new StompJs.Client({
webSocketFactory: () => new SockJS('http://localhost:8081/ws'),
connectHeaders: {
Authorization: `Bearer ${AUTH_TOKEN}`
},
onConnect: (frame) => {
document.getElementById('connection-status').textContent = '✅ 已连接';
// 订阅任务通知
client.subscribe('/user/queue/tasks-progress', (message) => {
const notification = JSON.parse(message.body);
handleTaskNotification(notification);
});
},
onStompError: (frame) => {
document.getElementById('connection-status').textContent = '❌ 连接错误';
console.error('STOMP 错误:', frame);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
// 处理任务通知
function handleTaskNotification(notification) {
const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
// 更新任务数据
tasks[taskNo] = notification;
// 渲染任务列表
renderTasks();
// 显示浏览器通知(如果支持)
if (status === 'completed' && 'Notification' in window && Notification.permission === 'granted') {
new Notification('任务完成', {
body: `任务 ${taskNo} 已成功完成!`,
icon: '/icon.png'
});
}
}
// 渲染任务列表
function renderTasks() {
const container = document.getElementById('tasks-container');
container.innerHTML = '';
Object.values(tasks).forEach(task => {
const taskDiv = document.createElement('div');
taskDiv.className = 'task-card';
taskDiv.innerHTML = `
<h3>${task.taskNo}</h3>
<p>状态: ${task.status}</p>
<p>进度: ${task.progress}%</p>
<progress value="${task.progress}" max="100"></progress>
<p>${task.message}</p>
${task.resultUrl ? `<a href="${task.resultUrl}" target="_blank">查看结果</a>` : ''}
${task.errorMessage ? `<p style="color:red">错误: ${task.errorMessage}</p>` : ''}
`;
container.appendChild(taskDiv);
});
}
// 激活连接
client.activate();
// 页面关闭时断开连接
window.addEventListener('beforeunload', () => {
if (client.connected) {
client.deactivate();
}
});
// 请求浏览器通知权限
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
</script>
<style>
.task-card {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
}
progress {
width: 100%;
height: 20px;
}
</style>
</body>
</html>
```
---
## 六、完整业务流程示例
```javascript
import TaskWebSocketClient from './TaskWebSocketClient';
import SuChuangImageGenerator from './SuChuangImageGenerator';
class AITaskManager {
constructor(token, baseUrl = 'http://localhost:8081') {
this.wsClient = new TaskWebSocketClient(baseUrl, token);
this.generator = new SuChuangImageGenerator(token, baseUrl);
this.pendingTasks = new Map();
}
/**
* 初始化(连接 WebSocket 并设置监听器)
*/
async init() {
// 监听任务更新
this.wsClient.onTaskUpdate((notification) => {
this.handleTaskUpdate(notification);
});
// 连接 WebSocket
await this.wsClient.connect();
console.log('AI 任务管理器已初始化');
}
/**
* 提交文生图任务
*/
async submitTextToImage(prompt, aspectRatio = '1:1', onProgress, onComplete, onError) {
try {
// 提交任务
const taskNo = await this.generator.generateImage(prompt, aspectRatio);
// 注册回调
this.pendingTasks.set(taskNo, { onProgress, onComplete, onError });
return taskNo;
} catch (error) {
if (onError) onError(error);
throw error;
}
}
/**
* 提交图生图任务
*/
async submitImageToImage(prompt, imageUrl, aspectRatio = '1:1', onProgress, onComplete, onError) {
try {
const taskNo = await this.generator.transformImage(prompt, imageUrl, aspectRatio);
this.pendingTasks.set(taskNo, { onProgress, onComplete, onError });
return taskNo;
} catch (error) {
if (onError) onError(error);
throw error;
}
}
/**
* 处理任务更新
*/
handleTaskUpdate(notification) {
const { taskNo, status, progress, message, resultUrl, errorMessage } = notification;
const callbacks = this.pendingTasks.get(taskNo);
if (!callbacks) return;
const { onProgress, onComplete, onError } = callbacks;
switch(status) {
case 'processing':
if (onProgress) onProgress(progress, message);
break;
case 'completed':
if (onComplete) onComplete(resultUrl);
this.pendingTasks.delete(taskNo);
break;
case 'failed':
if (onError) onError(new Error(errorMessage || message));
this.pendingTasks.delete(taskNo);
break;
}
}
/**
* 清理资源
*/
destroy() {
this.wsClient.disconnect();
this.pendingTasks.clear();
}
}
// 使用示例
const taskManager = new AITaskManager(YOUR_JWT_TOKEN);
// 初始化
await taskManager.init();
// 提交任务并监听进度
const taskNo = await taskManager.submitTextToImage(
'一只可爱的柴犬',
'1:1',
(progress, message) => {
console.log(`进度: ${progress}% - ${message}`);
updateProgressBar(progress);
},
(resultUrl) => {
console.log('生成完成:', resultUrl);
displayImage(resultUrl);
},
(error) => {
console.error('生成失败:', error);
showErrorMessage(error.message);
}
);
console.log('任务已提交:', taskNo);
```
---
## 七、常见问题
### 1. 跨域问题
如果前端和后端不在同一域名,确保后端已配置 CORS
```java
// WebSocketConfig.java 中已配置
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
```
### 2. 认证失败
确保在连接时传递了正确的 JWT Token
```javascript
connectHeaders: {
Authorization: `Bearer ${YOUR_JWT_TOKEN}`
}
```
### 3. 连接断开自动重连
STOMP 客户端已配置自动重连:
```javascript
reconnectDelay: 5000 // 5秒后自动重连
```
### 4. 心跳检测
防止连接超时:
```javascript
heartbeatIncoming: 4000, // 接收心跳间隔
heartbeatOutgoing: 4000 // 发送心跳间隔
```
---
## 八、调试技巧
### 1. 开启详细日志
```javascript
const client = new Client({
// ... 其他配置
debug: (str) => {
console.log('STOMP Debug:', str);
}
});
```
### 2. 监控连接状态
```javascript
client.onConnect = () => console.log('✅ 已连接');
client.onDisconnect = () => console.log('🔌 已断开');
client.onWebSocketError = (error) => console.error('❌ 错误:', error);
```
### 3. 测试消息接收
```javascript
client.subscribe('/user/queue/tasks-progress', (message) => {
console.log('收到原始消息:', message.body);
const data = JSON.parse(message.body);
console.log('解析后的数据:', data);
});
```
---
## 九、安全建议
1. **不要在前端暴露敏感信息**
- Token 应通过 localStorage 或 sessionStorage 安全存储
- 避免在 URL 中传递 Token
2. **设置合理的超时时间**
- 避免长时间保持空闲连接
3. **处理连接断开**
- 实现重连逻辑
- 提示用户连接状态
4. **验证消息来源**
- 确认 taskNo 是否为当前用户的任务
---
## 十、完整目录结构
```
src/
├── websocket/
│ ├── TaskWebSocketClient.js # WebSocket 封装类
│ └── AITaskManager.js # 任务管理器
├── api/
│ └── SuChuangImageGenerator.js # 任务提交API
├── components/
│ ├── TaskMonitor.jsx # React 任务监控组件
│ └── TaskMonitor.vue # Vue 任务监控组件
└── utils/
└── notification.js # 浏览器通知工具
```
---
## 总结
前端通过以下步骤接收 WebSocket 通知:
1. **连接**: `new SockJS('http://localhost:8081/ws')`
2. **认证**: 在 `connectHeaders` 中传递 JWT Token
3. **订阅**: `client.subscribe('/user/queue/tasks-progress', callback)`
4. **处理**: 根据 `status` 字段处理不同状态的通知
**关键点**
- ✅ 端点以 `/ws` 开头(不是 `/user/ws`
- ✅ 订阅地址为 `/user/queue/tasks-progress`
- ✅ Spring 会自动将消息路由到当前用户
- ✅ 支持自动重连和心跳检测