在vue3中使用webSocket构建聊天对话方式
作者:吃代码长大的前端
我们来构建一个功能完善的聊天通信应用。
这个应用使用现代技术栈:Electron + Vue 3 + Element Plus + SCSS + TypeScript,并实现一个健壮的、封装良好的 WebSocket 服务,包含心跳检查和断线重连机制。
下面是详细的步骤和代码实现。
第一步:项目初始化
我们将使用 electron-vite 这个现代化的构建工具来快速搭建项目。
创建项目:
npm create @quick-start/electron my-chat-app -- --template vue-ts
进入项目并安装依赖:
cd my-chat-app npm install
安装 Element Plus 和 SCSS/Sass:
npm install element-plus npm install -D sass
在 src/main.ts 中引入 Element Plus:
// src/renderer/src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' // 引入样式
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
```
### 第二步:封装 WebSocket 服务 (核心)
这是整个应用的核心。我们将创建一个 `WebSocketService` 类来管理所有 WebSocket 相关的逻辑,包括连接、断开、发送消息、心跳和重连。
在 `src/renderer/src/services` 目录下创建一个新文件 `webSocketService.ts`。
```typescript
// src/renderer/src/services/webSocketService.ts
import { reactive } from 'vue'
// 定义消息格式
interface WebSocketMessage {
type: 'heartbeat' | 'message' | 'auth'
payload: any
}
// 定义服务状态
type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed'
// 使用 reactive 创建响应式状态,方便 Vue 组件直接使用
export const wsState = reactive({
status: 'closed' as WebSocketStatus,
messages: [] as { id: number; text: string; sender: 'me' | 'other' }[],
})
class WebSocketService {
private url: string
private ws: WebSocket | null = null
private token:string
// 心跳相关配置
private heartbeatInterval: number = 30000 // 30秒发送一次心跳
private heartbeatTimer: NodeJS.Timeout | null = null
private serverTimeoutTimer: NodeJS.Timeout | null = null
// 重连相关配置
private reconnectTimeout: number = 5000 // 5秒后重连
private reconnectTimer: NodeJS.Timeout | null = null
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 5
constructor(url: string,token:string) {
this.url = url,this.token=token
}
// --- Public API ---
public connect(): void {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
console.log('WebSocket is already connected or connecting.')
return
}
wsState.status = 'connecting'
console.log('WebSocket connecting...')
this.ws = new WebSocket(this.url,this.token)
this.ws.onopen = () => this.onOpen()
this.ws.onmessage = (event) => this.onMessage(event)
this.ws.onclose = () => this.onClose()
this.ws.onerror = (error) => this.onError(error)
}
public disconnect(): void {
if (this.ws) {
console.log('WebSocket disconnecting...')
wsState.status = 'closing'
// 清理所有定时器
this.clearTimers()
this.ws.close()
}
}
public sendMessage(text: string): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message: WebSocketMessage = { type: 'message', payload: text }
this.ws.send(JSON.stringify(message))
// 将自己发的消息也添加到消息列表
wsState.messages.push({ id: Date.now(), text, sender: 'me' })
} else {
console.error('WebSocket is not open. Cannot send message.')
}
}
// --- Private Event Handlers ---
private onOpen(): void {
wsState.status = 'open'
console.log('WebSocket connection established.')
// 连接成功后,重置重连尝试次数
this.reconnectAttempts = 0
// 清除可能存在的重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
// 开启心跳
this.startHeartbeat()
}
private onMessage(event: MessageEvent): void {
// 收到任何消息都代表连接正常,重置心跳
this.resetHeartbeat()
const message = JSON.parse(event.data)
if (message.type === 'heartbeat' && message.payload === 'pong') {
// 收到心跳响应,不做处理,因为 resetHeartbeat 已经重置了定时器
console.log('Received pong from server.')
return
}
// 处理普通消息
if (message.type === 'message') {
wsState.messages.push({ id: Date.now(), text: message.payload, sender: 'other' })
}
}
private onClose(): void {
wsState.status = 'closed'
console.log('WebSocket connection closed.')
this.clearTimers()
// 触发重连机制
this.handleReconnect()
}
private onError(error: Event): void {
console.error('WebSocket error:', error)
// 错误发生时,ws.onclose 也通常会被调用,所以重连逻辑放在 onclose 中处理
}
// --- Heartbeat Mechanism ---
private startHeartbeat(): void {
console.log('Starting heartbeat...')
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const heartbeatMessage: WebSocketMessage = { type: 'heartbeat', payload: 'ping' }
this.ws.send(JSON.stringify(heartbeatMessage))
// 设置一个服务器超时定时器,如果规定时间内没收到 pong,则认为连接已断开
this.serverTimeoutTimer = setTimeout(() => {
console.warn("Server timeout. Closing connection.")
this.ws?.close() // 这会触发 onClose,然后由 onClose 触发重连
}, 5000) // 5秒内必须收到 pong
}
}, this.heartbeatInterval)
}
private resetHeartbeat(): void {
// 清除上一个心跳的服务器超时定时器
if (this.serverTimeoutTimer) {
clearTimeout(this.serverTimeoutTimer)
}
}
// --- Reconnection Mechanism ---
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
console.log(`Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
this.reconnectTimer = setTimeout(() => {
this.connect()
}, this.reconnectTimeout)
} else {
console.error('Max reconnection attempts reached. Please check the server or network.')
}
}
// --- Utility ---
private clearTimers(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
if (this.serverTimeoutTimer) {
clearTimeout(this.serverTimeoutTimer)
this.serverTimeoutTimer = null
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
}
}
// 创建并导出一个单例
token从store获取
// 这里的 URL 应该指向你的 WebSocket 服务器
const wsService = new WebSocketService('ws://localhost:8080',token)
export default wsService代码解析:
状态管理 (wsState): 使用 Vue 3 的 reactive API 创建了一个响应式对象。任何对 wsState.status 或 wsState.messages 的修改都会自动更新到 Vue 组件的 UI 上。
核心方法 (connect, disconnect, sendMessage):提供清晰的公共 API 给外部调用。
心跳机制 (startHeartbeat, resetHeartbeat):
startHeartbeat: 连接成功后,每隔 30 秒向服务器发送一个ping包。- 在发送
ping的同时,启动一个 5 秒的超时定时器 (serverTimeoutTimer)。 resetHeartbeat: 当收到服务器的任何消息(包括pong),就清除这个超时定时器。- 如果 5 秒内没有收到任何消息,超时定时器会触发,主动关闭连接,从而触发
onClose中的重连逻辑。这能有效检测到“假死”连接。
重连机制 (handleReconnect):
- 在
onClose事件中被调用。 - 设置了最大重连次数,避免无限重连。
- 使用
setTimeout延迟重连,给服务器和网络缓冲时间。
第二步:在 Vue 组件中使用 WebSocket 服务
现在,我们在 Vue 组件中使用这个封装好的服务。
修改 src/renderer/src/App.vue,或者创建一个新的聊天组件 Chat.vue。这里我们直接修改 App.vue。
<!-- src/renderer/src/App.vue -->
<template>
<div class="chat-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>WebSocket Chat</span>
<el-badge :value="statusText" :type="statusType" class="status-badge" />
</div>
</template>
<el-scrollbar ref="scrollbarRef" class="message-area">
<div v-for="msg in messages" :key="msg.id" class="message-item" :class="`is-${msg.sender}`">
<div class="message-bubble">{{ msg.text }}</div>
</div>
</el-scrollbar>
<div class="input-area">
<el-input
v-model="newMessage"
placeholder="Type a message..."
@keyup.enter="handleSendMessage"
:disabled="wsState.status !== 'open'"
/>
<el-button
type="primary"
@click="handleSendMessage"
:disabled="wsState.status !== 'open'"
>
Send
</el-button>
</div>
<div class="controls">
<el-button @click="wsService.connect()" :disabled="wsState.status === 'open' || wsState.status === 'connecting'">Connect</el-button>
<el-button @click="wsService.disconnect()" :disabled="wsState.status !== 'open'">Disconnect</el-button>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, computed, onMounted } from 'vue'
import wsService, { wsState } from './services/webSocketService'
import type { ElScrollbar } from 'element-plus'
const newMessage = ref('')
const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
// 直接从 service 中解构出响应式数据
const { messages } = wsState
// 自动滚动到底部
watch(messages, () => {
nextTick(() => {
scrollbarRef.value?.setScrollTop(scrollbarRef.value.wrapRef!.scrollHeight)
})
}, { deep: true })
const handleSendMessage = () => {
if (newMessage.value.trim()) {
wsService.sendMessage(newMessage.value)
newMessage.value = ''
}
}
// 根据连接状态显示不同的文本和颜色
const statusText = computed(() => {
switch (wsState.status) {
case 'open':
return 'Connected'
case 'connecting':
return 'Connecting'
case 'closing':
return 'Closing'
case 'closed':
return 'Disconnected'
default:
return 'Unknown'
}
})
const statusType = computed(() => {
switch (wsState.status) {
case 'open':
return 'success'
case 'connecting':
return 'warning'
default:
return 'info'
}
})
// 组件挂载后自动连接
onMounted(() => {
wsService.connect()
})
</script>
<style lang="scss">
body, html, #app {
height: 100%;
margin: 0;
background-color: #f0f2f5;
}
.chat-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
}
.box-card {
width: 600px;
height: 80vh;
display: flex;
flex-direction: column;
.el-card__header {
flex-shrink: 0;
}
.el-card__body {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 10px;
overflow: hidden;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.message-area {
flex-grow: 1;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin-bottom: 10px;
}
.message-item {
display: flex;
margin-bottom: 10px;
&.is-me {
justify-content: flex-end;
.message-bubble {
background-color: #409eff;
color: white;
}
}
&.is-other {
justify-content: flex-start;
.message-bubble {
background-color: #e9e9eb;
color: #333;
}
}
.message-bubble {
padding: 8px 12px;
border-radius: 10px;
max-width: 70%;
}
}
.input-area {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.controls {
margin-top: 10px;
display: flex;
justify-content: center;
gap: 10px;
flex-shrink: 0;
}
</style>组件解析:
- 导入服务:
import wsService, { wsState } from './services/webSocketService',我们导入了服务实例和它的响应式状态。 - 响应式绑定:
v-for="msg in wsState.messages"和wsState.status直接在模板中使用,当WebSocketService内部更新这些状态时,UI 会自动更新。 - 自动滚动: 使用
watch和nextTick确保每次有新消息时,聊天窗口都会自动滚动到底部。 - 状态展示: 使用
computed属性根据wsState.status动态地改变状态徽章的文本和颜色。 - 交互: "Send", "Connect", "Disconnect" 按钮直接调用
wsService上的方法。
第三步:创建一个简单的 WebSocket 后端服务器
为了测试,我们需要一个 WebSocket 服务器。你可以使用 Node.js 和 ws 包快速创建一个。
在项目根目录安装 ws:
npm install ws npm install -D @types/ws
在项目根目录创建 server.js:
// server.js
const { WebSocketServer } = require('ws')
const wss = new WebSocketServer({ port: 8080 })
console.log('WebSocket server is running on ws://localhost:8080')
wss.on('connection', function connection(ws) {
console.log('A new client connected!')
ws.send(JSON.stringify({ type: 'message', payload: 'Welcome to the chat!' }))
ws.on('message', function message(data) {
console.log('received: %s', data)
const parsedData = JSON.parse(data)
// 心跳处理
if (parsedData.type === 'heartbeat' && parsedData.payload === 'ping') {
ws.send(JSON.stringify({ type: 'heartbeat', payload: 'pong' }))
return
}
// 广播消息给所有客户端
wss.clients.forEach(function each(client) {
// 只发送给其他客户端
if (client !== ws && client.readyState === 1) { // 1 表示 WebSocket.OPEN
client.send(JSON.stringify({ type: 'message', payload: parsedData.payload }))
}
})
})
ws.on('close', () => {
console.log('Client disconnected.')
})
ws.on('error', (error) => {
console.error('WebSocket error:', error)
})
})
```
### 第五步:运行和测试
1. **启动 WebSocket 服务器:**
```bash
node server.js你会看到 WebSocket server is running on ws://localhost:8080。
启动 Electron 应用:
npm run dev
测试场景:
1.正常通信:打开两个 Electron 应用实例,它们应该能互相发送和接收消息。
2.心跳检查:在服务器控制台,你会看到每隔 30 秒收到 "ping" 消息。
3.断线重连:
- 手动点击 "Disconnect" 按钮,状态会变为 "Disconnected"。再点击 "Connect" 重新连接。
- 关键测试:运行应用后,关闭
node server.js进程。你会看到客户端 UI 上的状态变为 "Connecting",并尝试 5 次重连。在控制台可以看到重连日志。如果在它放弃之前重新启动服务器,客户端应该会自动连接成功。 - 心跳超时测试:如果你注释掉服务器代码中
ws.send(JSON.stringify({ type: 'heartbeat', payload: 'pong' }))这一行,客户端会在发送ping后 5 秒内因为收不到pong而主动断开并尝试重连。
总结与展望
这个方案提供了一个非常坚实的基础:
- 高内聚,低耦合: WebSocket 的所有复杂逻辑(状态、心跳、重连)都封装在
WebSocketService中,Vue 组件只负责展示 UI 和调用简单的 API,非常清晰。 - 响应式: 利用 Vue 3 的
reactive,数据流是单向且自动的,无需手动操作 DOM 或通过事件总线传递状态。 - 健壮性: 心跳机制能检测到网络假死,而自动重连则提升了用户体验,使应用能从临时的网络问题中恢复。
可扩展方向:
- 状态管理 (Pinia): 对于更复杂的应用,可以将
wsState移入 Pinia store,以便在多个组件和模块中更方便地共享和管理状态。 - 消息格式: 定义更丰富的消息类型,如用户列表更新、图片消息、文件消息等。
- 认证: 在
onOpen时,客户端可以发送一个认证令牌,服务器验证通过后才允许后续通信。 - 错误处理: 在 UI 上向用户展示更友好的错误信息(如“无法连接到服务器”)。
- WSS: 在生产环境中,应使用
wss://(WebSocket Secure) 协议来加密通信。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
