在vue3中使用webSocket構(gòu)建聊天對話方式
我們來構(gòu)建一個功能完善的聊天通信應(yīng)用。
這個應(yīng)用使用現(xiàn)代技術(shù)棧:Electron + Vue 3 + Element Plus + SCSS + TypeScript,并實現(xiàn)一個健壯的、封裝良好的 WebSocket 服務(wù),包含心跳檢查和斷線重連機制。
下面是詳細的步驟和代碼實現(xiàn)。
第一步:項目初始化
我們將使用 electron-vite 這個現(xiàn)代化的構(gòu)建工具來快速搭建項目。
創(chuàng)建項目:
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 服務(wù) (核心)
這是整個應(yīng)用的核心。我們將創(chuàng)建一個 `WebSocketService` 類來管理所有 WebSocket 相關(guān)的邏輯,包括連接、斷開、發(fā)送消息、心跳和重連。
在 `src/renderer/src/services` 目錄下創(chuàng)建一個新文件 `webSocketService.ts`。
```typescript
// src/renderer/src/services/webSocketService.ts
import { reactive } from 'vue'
// 定義消息格式
interface WebSocketMessage {
type: 'heartbeat' | 'message' | 'auth'
payload: any
}
// 定義服務(wù)狀態(tài)
type WebSocketStatus = 'connecting' | 'open' | 'closing' | 'closed'
// 使用 reactive 創(chuàng)建響應(yīng)式狀態(tài),方便 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
// 心跳相關(guān)配置
private heartbeatInterval: number = 30000 // 30秒發(fā)送一次心跳
private heartbeatTimer: NodeJS.Timeout | null = null
private serverTimeoutTimer: NodeJS.Timeout | null = null
// 重連相關(guān)配置
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))
// 將自己發(fā)的消息也添加到消息列表
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.')
// 連接成功后,重置重連嘗試次數(shù)
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') {
// 收到心跳響應(yīng),不做處理,因為 resetHeartbeat 已經(jīng)重置了定時器
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()
// 觸發(fā)重連機制
this.handleReconnect()
}
private onError(error: Event): void {
console.error('WebSocket error:', error)
// 錯誤發(fā)生時,ws.onclose 也通常會被調(diào)用,所以重連邏輯放在 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))
// 設(shè)置一個服務(wù)器超時定時器,如果規(guī)定時間內(nèi)沒收到 pong,則認為連接已斷開
this.serverTimeoutTimer = setTimeout(() => {
console.warn("Server timeout. Closing connection.")
this.ws?.close() // 這會觸發(fā) onClose,然后由 onClose 觸發(fā)重連
}, 5000) // 5秒內(nèi)必須收到 pong
}
}, this.heartbeatInterval)
}
private resetHeartbeat(): void {
// 清除上一個心跳的服務(wù)器超時定時器
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
}
}
}
// 創(chuàng)建并導(dǎo)出一個單例
token從store獲取
// 這里的 URL 應(yīng)該指向你的 WebSocket 服務(wù)器
const wsService = new WebSocketService('ws://localhost:8080',token)
export default wsService代碼解析:
狀態(tài)管理 (wsState): 使用 Vue 3 的 reactive API 創(chuàng)建了一個響應(yīng)式對象。任何對 wsState.status 或 wsState.messages 的修改都會自動更新到 Vue 組件的 UI 上。
核心方法 (connect, disconnect, sendMessage):提供清晰的公共 API 給外部調(diào)用。
心跳機制 (startHeartbeat, resetHeartbeat):
startHeartbeat: 連接成功后,每隔 30 秒向服務(wù)器發(fā)送一個ping包。- 在發(fā)送
ping的同時,啟動一個 5 秒的超時定時器 (serverTimeoutTimer)。 resetHeartbeat: 當收到服務(wù)器的任何消息(包括pong),就清除這個超時定時器。- 如果 5 秒內(nèi)沒有收到任何消息,超時定時器會觸發(fā),主動關(guān)閉連接,從而觸發(fā)
onClose中的重連邏輯。這能有效檢測到“假死”連接。
重連機制 (handleReconnect):
- 在
onClose事件中被調(diào)用。 - 設(shè)置了最大重連次數(shù),避免無限重連。
- 使用
setTimeout延遲重連,給服務(wù)器和網(wǎng)絡(luò)緩沖時間。
第二步:在 Vue 組件中使用 WebSocket 服務(wù)
現(xiàn)在,我們在 Vue 組件中使用這個封裝好的服務(wù)。
修改 src/renderer/src/App.vue,或者創(chuàng)建一個新的聊天組件 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 中解構(gòu)出響應(yīng)式數(shù)據(jù)
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 = ''
}
}
// 根據(jù)連接狀態(tài)顯示不同的文本和顏色
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>組件解析:
- 導(dǎo)入服務(wù):
import wsService, { wsState } from './services/webSocketService',我們導(dǎo)入了服務(wù)實例和它的響應(yīng)式狀態(tài)。 - 響應(yīng)式綁定:
v-for="msg in wsState.messages"和wsState.status直接在模板中使用,當WebSocketService內(nèi)部更新這些狀態(tài)時,UI 會自動更新。 - 自動滾動: 使用
watch和nextTick確保每次有新消息時,聊天窗口都會自動滾動到底部。 - 狀態(tài)展示: 使用
computed屬性根據(jù)wsState.status動態(tài)地改變狀態(tài)徽章的文本和顏色。 - 交互: "Send", "Connect", "Disconnect" 按鈕直接調(diào)用
wsService上的方法。
第三步:創(chuàng)建一個簡單的 WebSocket 后端服務(wù)器
為了測試,我們需要一個 WebSocket 服務(wù)器。你可以使用 Node.js 和 ws 包快速創(chuàng)建一個。
在項目根目錄安裝 ws:
npm install ws npm install -D @types/ws
在項目根目錄創(chuàng)建 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) {
// 只發(fā)送給其他客戶端
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 服務(wù)器:**
```bash
node server.js你會看到 WebSocket server is running on ws://localhost:8080。
啟動 Electron 應(yīng)用:
npm run dev
測試場景:
1.正常通信:打開兩個 Electron 應(yīng)用實例,它們應(yīng)該能互相發(fā)送和接收消息。
2.心跳檢查:在服務(wù)器控制臺,你會看到每隔 30 秒收到 "ping" 消息。
3.斷線重連:
- 手動點擊 "Disconnect" 按鈕,狀態(tài)會變?yōu)?"Disconnected"。再點擊 "Connect" 重新連接。
- 關(guān)鍵測試:運行應(yīng)用后,關(guān)閉
node server.js進程。你會看到客戶端 UI 上的狀態(tài)變?yōu)?"Connecting",并嘗試 5 次重連。在控制臺可以看到重連日志。如果在它放棄之前重新啟動服務(wù)器,客戶端應(yīng)該會自動連接成功。 - 心跳超時測試:如果你注釋掉服務(wù)器代碼中
ws.send(JSON.stringify({ type: 'heartbeat', payload: 'pong' }))這一行,客戶端會在發(fā)送ping后 5 秒內(nèi)因為收不到pong而主動斷開并嘗試重連。
總結(jié)與展望
這個方案提供了一個非常堅實的基礎(chǔ):
- 高內(nèi)聚,低耦合: WebSocket 的所有復(fù)雜邏輯(狀態(tài)、心跳、重連)都封裝在
WebSocketService中,Vue 組件只負責展示 UI 和調(diào)用簡單的 API,非常清晰。 - 響應(yīng)式: 利用 Vue 3 的
reactive,數(shù)據(jù)流是單向且自動的,無需手動操作 DOM 或通過事件總線傳遞狀態(tài)。 - 健壯性: 心跳機制能檢測到網(wǎng)絡(luò)假死,而自動重連則提升了用戶體驗,使應(yīng)用能從臨時的網(wǎng)絡(luò)問題中恢復(fù)。
可擴展方向:
- 狀態(tài)管理 (Pinia): 對于更復(fù)雜的應(yīng)用,可以將
wsState移入 Pinia store,以便在多個組件和模塊中更方便地共享和管理狀態(tài)。 - 消息格式: 定義更豐富的消息類型,如用戶列表更新、圖片消息、文件消息等。
- 認證: 在
onOpen時,客戶端可以發(fā)送一個認證令牌,服務(wù)器驗證通過后才允許后續(xù)通信。 - 錯誤處理: 在 UI 上向用戶展示更友好的錯誤信息(如“無法連接到服務(wù)器”)。
- WSS: 在生產(chǎn)環(huán)境中,應(yīng)使用
wss://(WebSocket Secure) 協(xié)議來加密通信。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue3.0中使用websocket,封裝到公共方法的實現(xiàn)
這篇文章主要介紹了vue3.0中使用websocket,封裝到公共方法的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
淺談vue中使用編輯器vue-quill-editor踩過的坑
這篇文章主要介紹了淺談vue中使用編輯器vue-quill-editor踩過的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
@vue/cli4升級@vue/cli5?node.js?polyfills錯誤的解決方式
最近在升級vue/cli的具有了一些問題,解決問題的過程也花費了些時間,所以下面這篇文章主要給大家介紹了關(guān)于@vue/cli4升級@vue/cli5?node.js?polyfills錯誤的解決方式,需要的朋友可以參考下2022-09-09
VUE側(cè)邊導(dǎo)航欄實現(xiàn)篩選過濾的示例代碼
本文主要介紹了VUE側(cè)邊導(dǎo)航欄實現(xiàn)篩選過濾的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧2023-05-05
Vue3 openlayers加載瓦片地圖并手動標記坐標點功能
這篇文章主要介紹了 Vue3 openlayers加載瓦片地圖并手動標記坐標點功能,我們這里用vue/cli創(chuàng)建,我用的node版本是18.12.1,本文結(jié)合示例代碼給大家介紹的非常詳細,需要的朋友可以參考下2024-04-04

