608 lines
12 KiB
Markdown
608 lines
12 KiB
Markdown
|
|
# WebSocket 使用示例
|
|||
|
|
|
|||
|
|
## 📦 已创建的文件
|
|||
|
|
|
|||
|
|
✅ `/utils/websocket-manager.js` - WebSocket 管理器(已完成)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 快速开始
|
|||
|
|
|
|||
|
|
### 1. 在 App.vue 中初始化连接
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<script>
|
|||
|
|
import wsManager from '@/utils/websocket-manager.js'
|
|||
|
|
import { mapState } from 'vuex'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
computed: {
|
|||
|
|
...mapState(['vuex_token', 'vuex_user'])
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLaunch() {
|
|||
|
|
console.log('App Launch')
|
|||
|
|
|
|||
|
|
// 如果已登录,连接 WebSocket
|
|||
|
|
if (this.vuex_token) {
|
|||
|
|
this.connectWebSocket()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
connectWebSocket() {
|
|||
|
|
// 替换为你的 WebSocket 服务器地址
|
|||
|
|
const wsUrl = `wss://your-server.com/chat?token=${this.vuex_token}`
|
|||
|
|
|
|||
|
|
wsManager.connect(wsUrl, {
|
|||
|
|
reconnectInterval: 3000, // 重连间隔 3 秒
|
|||
|
|
maxReconnectAttempts: 10, // 最多重连 10 次
|
|||
|
|
heartbeatInterval: 30000 // 心跳间隔 30 秒
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听连接成功
|
|||
|
|
wsManager.on('open', () => {
|
|||
|
|
console.log('[App] WebSocket 连接成功')
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已连接',
|
|||
|
|
icon: 'success'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听连接关闭
|
|||
|
|
wsManager.on('close', (res) => {
|
|||
|
|
console.log('[App] WebSocket 已关闭', res)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听连接错误
|
|||
|
|
wsManager.on('error', (err) => {
|
|||
|
|
console.error('[App] WebSocket 错误', err)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '连接失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听重连
|
|||
|
|
wsManager.on('reconnect', (attempts) => {
|
|||
|
|
console.log(`[App] 正在重连 (${attempts})`)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听消息(全局消息处理)
|
|||
|
|
wsManager.on('message', (data) => {
|
|||
|
|
console.log('[App] 收到消息:', data)
|
|||
|
|
|
|||
|
|
// 根据消息类型处理
|
|||
|
|
switch (data.type) {
|
|||
|
|
case 'message':
|
|||
|
|
// 新消息
|
|||
|
|
this.handleNewMessage(data)
|
|||
|
|
break
|
|||
|
|
case 'notification':
|
|||
|
|
// 系统通知
|
|||
|
|
this.handleNotification(data)
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
console.log('[App] 未知消息类型:', data.type)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
handleNewMessage(data) {
|
|||
|
|
// 更新 Vuex 中的消息列表
|
|||
|
|
this.$store.commit('addMessage', data)
|
|||
|
|
|
|||
|
|
// 显示新消息提示
|
|||
|
|
uni.showTabBarRedDot({
|
|||
|
|
index: 1 // 消息页的 tabBar 索引
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
handleNotification(data) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: data.content,
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onHide() {
|
|||
|
|
// App 进入后台,保持连接
|
|||
|
|
console.log('App Hide')
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onShow() {
|
|||
|
|
// App 从后台回到前台
|
|||
|
|
console.log('App Show')
|
|||
|
|
|
|||
|
|
// 如果连接已断开,重新连接
|
|||
|
|
const state = wsManager.getState()
|
|||
|
|
if (!state.isConnected && !state.isConnecting) {
|
|||
|
|
this.connectWebSocket()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. 在聊天页面中使用
|
|||
|
|
|
|||
|
|
#### 方式 A:修改现有的 `dialogBox.vue`
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view>
|
|||
|
|
<!-- 消息列表 -->
|
|||
|
|
<scroll-view
|
|||
|
|
:scroll-top="scrollTop"
|
|||
|
|
:scroll-with-animation="true"
|
|||
|
|
class="scroll"
|
|||
|
|
scroll-y="true"
|
|||
|
|
>
|
|||
|
|
<view v-for="(msg, index) in messageList" :key="index" class="message-item">
|
|||
|
|
<!-- 我方消息 -->
|
|||
|
|
<view v-if="msg.fromUserId == vuex_user.id" class="my-message">
|
|||
|
|
<text>{{ msg.content }}</text>
|
|||
|
|
</view>
|
|||
|
|
<!-- 对方消息 -->
|
|||
|
|
<view v-else class="other-message">
|
|||
|
|
<text>{{ msg.content }}</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<!-- 输入框 -->
|
|||
|
|
<view class="input-box">
|
|||
|
|
<input
|
|||
|
|
v-model="inputValue"
|
|||
|
|
placeholder="请输入消息"
|
|||
|
|
@confirm="sendMessage"
|
|||
|
|
/>
|
|||
|
|
<button @click="sendMessage">发送</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import wsManager from '@/utils/websocket-manager.js'
|
|||
|
|
import { mapState } from 'vuex'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
messageList: [],
|
|||
|
|
inputValue: '',
|
|||
|
|
scrollTop: 0,
|
|||
|
|
targetUserId: '', // 对方用户 ID
|
|||
|
|
messageHandler: null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
computed: {
|
|||
|
|
...mapState(['vuex_user'])
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad(options) {
|
|||
|
|
// 获取对方用户 ID
|
|||
|
|
this.targetUserId = options.userId || ''
|
|||
|
|
|
|||
|
|
// 加载历史消息
|
|||
|
|
this.loadHistoryMessages()
|
|||
|
|
|
|||
|
|
// 监听新消息
|
|||
|
|
this.messageHandler = (data) => {
|
|||
|
|
// 只处理与当前聊天对象相关的消息
|
|||
|
|
if (data.fromUserId === this.targetUserId || data.toUserId === this.targetUserId) {
|
|||
|
|
this.messageList.push({
|
|||
|
|
fromUserId: data.fromUserId,
|
|||
|
|
toUserId: data.toUserId,
|
|||
|
|
content: data.content,
|
|||
|
|
timestamp: data.timestamp
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 滚动到底部
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.scrollToBottom()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 发送已读回执
|
|||
|
|
this.sendReadReceipt(data.messageId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
wsManager.on('message', this.messageHandler)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onUnload() {
|
|||
|
|
// 移除消息监听
|
|||
|
|
if (this.messageHandler) {
|
|||
|
|
wsManager.off('message', this.messageHandler)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 加载历史消息
|
|||
|
|
async loadHistoryMessages() {
|
|||
|
|
try {
|
|||
|
|
const res = await this.$u.api.getMessages({
|
|||
|
|
userId: this.targetUserId,
|
|||
|
|
page: 1,
|
|||
|
|
pageSize: 50
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.messageList = res.data || []
|
|||
|
|
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.scrollToBottom()
|
|||
|
|
})
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载历史消息失败:', e)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 发送消息
|
|||
|
|
sendMessage() {
|
|||
|
|
if (!this.inputValue.trim()) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请输入消息',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通过 WebSocket 发送
|
|||
|
|
wsManager.send({
|
|||
|
|
type: 'message',
|
|||
|
|
fromUserId: this.vuex_user.id,
|
|||
|
|
toUserId: this.targetUserId,
|
|||
|
|
content: this.inputValue,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 立即显示到界面(乐观更新)
|
|||
|
|
this.messageList.push({
|
|||
|
|
fromUserId: this.vuex_user.id,
|
|||
|
|
toUserId: this.targetUserId,
|
|||
|
|
content: this.inputValue,
|
|||
|
|
timestamp: Date.now(),
|
|||
|
|
sending: true // 标记为发送中
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 清空输入框
|
|||
|
|
this.inputValue = ''
|
|||
|
|
|
|||
|
|
// 滚动到底部
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.scrollToBottom()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 发送已读回执
|
|||
|
|
sendReadReceipt(messageId) {
|
|||
|
|
wsManager.send({
|
|||
|
|
type: 'read',
|
|||
|
|
messageId: messageId,
|
|||
|
|
userId: this.vuex_user.id
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 滚动到底部
|
|||
|
|
scrollToBottom() {
|
|||
|
|
this.scrollTop = 99999
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. 在 Vuex 中管理 WebSocket 状态(可选)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// store/index.js
|
|||
|
|
export default new Vuex.Store({
|
|||
|
|
state: {
|
|||
|
|
// ... 其他状态
|
|||
|
|
wsConnected: false,
|
|||
|
|
unreadCount: 0,
|
|||
|
|
messages: []
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
mutations: {
|
|||
|
|
// WebSocket 连接状态
|
|||
|
|
SET_WS_CONNECTED(state, connected) {
|
|||
|
|
state.wsConnected = connected
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 新消息
|
|||
|
|
ADD_MESSAGE(state, message) {
|
|||
|
|
state.messages.push(message)
|
|||
|
|
state.unreadCount++
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 清空未读
|
|||
|
|
CLEAR_UNREAD(state) {
|
|||
|
|
state.unreadCount = 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
actions: {
|
|||
|
|
// 在 App.vue 中调用
|
|||
|
|
wsConnected({ commit }) {
|
|||
|
|
commit('SET_WS_CONNECTED', true)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
wsDisconnected({ commit }) {
|
|||
|
|
commit('SET_WS_CONNECTED', false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 消息协议设计
|
|||
|
|
|
|||
|
|
### 客户端 → 服务器
|
|||
|
|
|
|||
|
|
#### 1. 发送文字消息
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "message",
|
|||
|
|
"fromUserId": "user123",
|
|||
|
|
"toUserId": "user456",
|
|||
|
|
"content": "你好",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 发送心跳
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "ping",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. 发送已读回执
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "read",
|
|||
|
|
"messageId": "msg123",
|
|||
|
|
"userId": "user123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 服务器 → 客户端
|
|||
|
|
|
|||
|
|
#### 1. 推送消息
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "message",
|
|||
|
|
"messageId": "msg123",
|
|||
|
|
"fromUserId": "user456",
|
|||
|
|
"toUserId": "user123",
|
|||
|
|
"content": "你好",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 心跳响应
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "pong",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. 系统通知
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "notification",
|
|||
|
|
"content": "系统维护通知",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. 错误消息
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "error",
|
|||
|
|
"code": 401,
|
|||
|
|
"message": "未授权",
|
|||
|
|
"timestamp": 1635678901234
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 后端实现参考
|
|||
|
|
|
|||
|
|
### Node.js + ws
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const WebSocket = require('ws')
|
|||
|
|
const wss = new WebSocket.Server({ port: 8080 })
|
|||
|
|
|
|||
|
|
// 存储所有连接(用户 ID -> WebSocket)
|
|||
|
|
const clients = new Map()
|
|||
|
|
|
|||
|
|
wss.on('connection', (ws, req) => {
|
|||
|
|
// 从 URL 获取 token
|
|||
|
|
const token = new URL(req.url, 'http://localhost').searchParams.get('token')
|
|||
|
|
|
|||
|
|
// 验证 token,获取用户 ID
|
|||
|
|
const userId = verifyToken(token)
|
|||
|
|
|
|||
|
|
if (!userId) {
|
|||
|
|
ws.close(4001, '未授权')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存连接
|
|||
|
|
clients.set(userId, ws)
|
|||
|
|
console.log(`用户 ${userId} 已连接`)
|
|||
|
|
|
|||
|
|
// 处理消息
|
|||
|
|
ws.on('message', (message) => {
|
|||
|
|
const data = JSON.parse(message)
|
|||
|
|
|
|||
|
|
switch (data.type) {
|
|||
|
|
case 'message':
|
|||
|
|
// 转发消息
|
|||
|
|
handleMessage(data)
|
|||
|
|
break
|
|||
|
|
case 'ping':
|
|||
|
|
// 心跳响应
|
|||
|
|
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }))
|
|||
|
|
break
|
|||
|
|
case 'read':
|
|||
|
|
// 已读回执
|
|||
|
|
handleReadReceipt(data)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 处理断开
|
|||
|
|
ws.on('close', () => {
|
|||
|
|
clients.delete(userId)
|
|||
|
|
console.log(`用户 ${userId} 已断开`)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 转发消息
|
|||
|
|
function handleMessage(data) {
|
|||
|
|
const { toUserId, fromUserId, content } = data
|
|||
|
|
|
|||
|
|
// 保存到数据库
|
|||
|
|
saveMessage({ fromUserId, toUserId, content })
|
|||
|
|
|
|||
|
|
// 推送给接收者
|
|||
|
|
const targetWs = clients.get(toUserId)
|
|||
|
|
if (targetWs && targetWs.readyState === WebSocket.OPEN) {
|
|||
|
|
targetWs.send(JSON.stringify({
|
|||
|
|
type: 'message',
|
|||
|
|
messageId: generateId(),
|
|||
|
|
fromUserId,
|
|||
|
|
toUserId,
|
|||
|
|
content,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 完整流程图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ App 启动 │
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 检查登录状态 │
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
▼ (已登录)
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 连接 WS │ ← wsManager.connect()
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 监听消息 │ ← wsManager.on('message')
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
├─→ (收到消息) → 更新 UI
|
|||
|
|
│
|
|||
|
|
└─→ (发送消息) → wsManager.send()
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 服务器处理 │
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 推送给对方 │
|
|||
|
|
└─────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ✅ 测试清单
|
|||
|
|
|
|||
|
|
### 功能测试
|
|||
|
|
- [ ] 连接成功
|
|||
|
|
- [ ] 发送消息
|
|||
|
|
- [ ] 接收消息
|
|||
|
|
- [ ] 心跳正常
|
|||
|
|
- [ ] 手动断开
|
|||
|
|
- [ ] 自动重连
|
|||
|
|
- [ ] 消息队列
|
|||
|
|
|
|||
|
|
### 场景测试
|
|||
|
|
- [ ] App 切换到后台
|
|||
|
|
- [ ] App 从后台恢复
|
|||
|
|
- [ ] 网络断开
|
|||
|
|
- [ ] 网络恢复
|
|||
|
|
- [ ] 服务器重启
|
|||
|
|
- [ ] 并发多人聊天
|
|||
|
|
|
|||
|
|
### 性能测试
|
|||
|
|
- [ ] 消息延迟 < 200ms
|
|||
|
|
- [ ] 重连时间 < 5s
|
|||
|
|
- [ ] 内存占用正常
|
|||
|
|
- [ ] CPU 占用正常
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🎉 总结
|
|||
|
|
|
|||
|
|
现在你已经有:
|
|||
|
|
|
|||
|
|
1. ✅ **WebSocket 管理器** (`utils/websocket-manager.js`)
|
|||
|
|
- 自动重连
|
|||
|
|
- 心跳检测
|
|||
|
|
- 消息队列
|
|||
|
|
- 事件监听
|
|||
|
|
|
|||
|
|
2. ✅ **使用示例**
|
|||
|
|
- App.vue 初始化
|
|||
|
|
- 聊天页面使用
|
|||
|
|
- Vuex 状态管理
|
|||
|
|
|
|||
|
|
3. ✅ **消息协议**
|
|||
|
|
- 客户端 → 服务器
|
|||
|
|
- 服务器 → 客户端
|
|||
|
|
|
|||
|
|
4. ✅ **后端参考**
|
|||
|
|
- Node.js + ws 实现
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 下一步
|
|||
|
|
|
|||
|
|
1. **确认后端技术栈**
|
|||
|
|
- 告诉我你用什么后端,我帮你写完整的服务端代码
|
|||
|
|
|
|||
|
|
2. **集成到现有聊天页面**
|
|||
|
|
- 我帮你修改 `dialogBox.vue`
|
|||
|
|
|
|||
|
|
3. **添加更多功能**
|
|||
|
|
- 图片消息
|
|||
|
|
- 语音消息
|
|||
|
|
- 消息已读
|
|||
|
|
- 输入中状态
|
|||
|
|
|
|||
|
|
随时告诉我需要什么!🎯
|
|||
|
|
|