vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue3使用webSocket构建聊天对话

在vue3中使用webSocket构建聊天对话方式

作者:吃代码长大的前端

本文介绍了如何使用Electron、Vue3、ElementPlus、SCSS和TypeScript构建一个功能完善的聊天通信应用,包括项目初始化、WebSocket服务的封装、心跳检查、断线重连机制,以及在Vue组件中使用该服务,此外,还展示了如何创建一个简单的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 上。

核心方法 (connectdisconnectsendMessage):提供清晰的公共 API 给外部调用。

心跳机制 (startHeartbeatresetHeartbeat):

重连机制 (handleReconnect):

第二步:在 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>

组件解析:

第三步:创建一个简单的 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.断线重连

总结与展望

这个方案提供了一个非常坚实的基础:

可扩展方向:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文