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

 更新时间:2025年10月27日 09:41:57   作者:吃代码长大的前端  
本文介绍了如何使用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):

  • 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) 协议来加密通信。

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

相关文章

  • vue3.0中使用websocket,封装到公共方法的实现

    vue3.0中使用websocket,封装到公共方法的实现

    这篇文章主要介绍了vue3.0中使用websocket,封装到公共方法的实现,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • 使用vue重构资讯页面的实例代码解析

    使用vue重构资讯页面的实例代码解析

    这篇文章主要介绍了使用vue重构资讯页面的实例代码解析,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-11-11
  • 浅谈vue中使用编辑器vue-quill-editor踩过的坑

    浅谈vue中使用编辑器vue-quill-editor踩过的坑

    这篇文章主要介绍了浅谈vue中使用编辑器vue-quill-editor踩过的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • 基于Vue-cli快速搭建项目的完整步骤

    基于Vue-cli快速搭建项目的完整步骤

    这篇文章主要给大家介绍了关于如何基于Vue-cli快速搭建项目的完整步骤,文中通过示例代码以及图片将搭建的步骤一步步介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-11-11
  • 源码剖析Vue3中如何进行错误处理

    源码剖析Vue3中如何进行错误处理

    错误处理是框架设计的核心要素之一,框架的错误处理好坏,直接决定用户应用程序的健壮性以及用户开发应用时处理错误的心智负担,本文将从源码入手,剖析一下Vue3中是如何进行错误处理的,需要的可以参考下
    2024-01-01
  • @vue/cli4升级@vue/cli5 node.js polyfills错误的解决方式

    @vue/cli4升级@vue/cli5 node.js polyfills错误的解决方式

    最近在升级vue/cli的具有了一些问题,解决问题的过程也花费了些时间,所以下面这篇文章主要给大家介绍了关于@vue/cli4升级@vue/cli5 node.js polyfills错误的解决方式,需要的朋友可以参考下
    2022-09-09
  • VUE侧边导航栏实现筛选过滤的示例代码

    VUE侧边导航栏实现筛选过滤的示例代码

    本文主要介绍了VUE侧边导航栏实现筛选过滤的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • vue实现动态列表点击各行换色的方法

    vue实现动态列表点击各行换色的方法

    今天小编就为大家分享一篇vue实现动态列表点击各行换色的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09
  • Vue3 openlayers加载瓦片地图并手动标记坐标点功能

    Vue3 openlayers加载瓦片地图并手动标记坐标点功能

    这篇文章主要介绍了 Vue3 openlayers加载瓦片地图并手动标记坐标点功能,我们这里用vue/cli创建,我用的node版本是18.12.1,本文结合示例代码给大家介绍的非常详细,需要的朋友可以参考下
    2024-04-04
  • Vue 仿百度搜索功能实现代码

    Vue 仿百度搜索功能实现代码

    本文通过实例代码给大家介绍了vue仿百度搜索功能,非常不错,具有参考借鉴价值,需要的的朋友参考下吧
    2017-02-02

最新评论