Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例

标签:WebSocket, Vue2, Node.js, ws, 心跳机制, 自动重连, 前后端通信


摘要

  • ✅ 基础 WebSocket 连接
  • ✅ 双向消息收发
  • ✅ 心跳检测(防止连接超时)
  • ✅ 客户端自动重连机制
  • ✅ 错误处理与状态管理
  • ✅ 可扩展的模块化设计

一、需求分析

1.1 项目背景

在现代 Web 应用中,实时性需求日益增长,如在线聊天、实时通知、数据监控等场景。传统的 HTTP 请求-响应模式无法满足低延迟、高并发的实时通信需求。WebSocket 提供了全双工通信通道,是实现实时功能的理想选择。

1.2 核心功能需求

功能 描述
基础通信 前端 Vue2 页面与后端 Node.js 建立 WebSocket 连接,实现双向消息发送与接收
心跳机制 客户端定时向服务端发送心跳包,服务端响应,防止因网络空闲导致连接被中间代理或防火墙断开
自动重连 客户端检测到连接断开后,自动尝试重新连接,支持重试次数与间隔控制
连接状态管理 前端展示连接状态(连接中、已连接、断开、重连中)
错误处理 捕获连接异常、消息解析错误等,并提供用户反馈
可扩展性 代码结构清晰,便于后续扩展更多业务逻辑(如鉴权、房间、广播等)

1.3 非功能需求

  • 稳定性:连接应尽可能保持稳定,断线能自动恢复
  • 可维护性:前后端代码模块化,易于理解和维护
  • 可测试性:提供简单 UI 验证功能

二、技术选型与介绍

2.1 技术栈

层级 技术 说明
前端 Vue2 + Vue CLI 主流的前端框架,适合构建单页应用
前端通信 原生 WebSocket API 浏览器原生支持,无需额外库
后端 Node.js + ws 轻量级、高性能的 WebSocket 库,社区活跃
包管理 npm / yarn 依赖管理
构建工具 Webpack (via Vue CLI) 前端打包

2.2 为什么选择 ws

  • ws 是 Node.js 中最流行的 WebSocket 实现之一,性能优秀,API 简洁。
  • 支持 WebSocket 协议的所有特性(包括子协议、扩展等)。
  • 文档完善,社区支持良好。
  • 轻量,不依赖 Express 等框架,可独立运行。

2.3 为什么使用原生 WebSocket 而非 Socket.IO

虽然 Socket.IO 提供了自动重连、降级支持等便利功能,但本项目目标是:

  • 深入理解 WebSocket 原理:手动实现心跳与重连,有助于掌握底层机制。
  • 轻量化:避免引入大型库的开销,适合学习和轻量级场景。
  • 可控性更强:自定义重连策略、心跳间隔等。

三、项目结构

bash 复制代码
websocket-demo/
├── backend/
│   ├── server.js          # WebSocket 服务端
│   └── package.json
├── frontend/
│   ├── src/
│   │   ├── main.js
│   │   ├── App.vue
│   │   ├── components/
│   │   │   └── WebSocketClient.vue  # 核心 WebSocket 客户端组件
│   │   └── utils/
│   │       └── WebSocketService.js  # 封装的 WebSocket 服务类
│   └── package.json
└── README.md

四、后端实现(Node.js + ws)

4.1 初始化项目

bash 复制代码
mkdir backend && cd backend
npm init -y
npm install ws

4.2 编写 WebSocket 服务端 (server.js)

javascript 复制代码
const WebSocket = require('ws');

// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 });

// 存储所有客户端连接
const clients = new Set();

wss.on('connection', (ws) => {
  console.log('New client connected');
  clients.add(ws);

  // 设置心跳响应
  ws.isAlive = true; // 标记客户端是否存活
  ws.on('pong', () => {
    ws.isAlive = true;
  });

  // 监听客户端消息
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data);
      console.log('Received:', message);

      // 广播消息给所有客户端(除发送者外)
      clients.forEach((client) => {
        if (client !== ws && client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify({
            type: 'message',
            data: message.data,
            from: 'server',
            timestamp: new Date().toISOString()
          }));
        }
      });
    } catch (err) {
      console.error('Error parsing message:', err);
    }
  });

  // 连接关闭
  ws.on('close', () => {
    console.log('Client disconnected');
    clients.delete(ws);
  });

  // 连接错误
  ws.on('error', (err) => {
    console.error('Client error:', err);
  });
});

// 心跳检测:每 30 秒检查一次客户端是否响应
const heartbeatInterval = setInterval(() => {
  clients.forEach((ws) => {
    if (!ws.isAlive) {
      console.log('Client not responding, terminating connection');
      return ws.terminate(); // 强制关闭无响应连接
    }
    ws.isAlive = false; // 下次心跳前未收到 pong 则标记为 false
    ws.ping(() => {}); // 发送 ping,callback 可选
  });
}, 30000); // 30 秒一次心跳检测

// 清理定时器
wss.on('close', () => {
  clearInterval(heartbeatInterval);
});

console.log('WebSocket server is running on ws://localhost:8080');

4.3 后端核心逻辑说明

  • isAlive 标志:用于标记客户端是否存活。
  • ping/pong 机制 :服务端发送 ping,客户端自动回复 pong,通过 on('pong') 监听。
  • 心跳定时器 :每 30 秒遍历所有客户端,若未收到 pong,则 isAlivefalse,强制关闭连接。
  • 消息广播:收到消息后,解析并转发给其他在线客户端。

五、前端实现(Vue2)

5.1 初始化 Vue2 项目

bash 复制代码
vue create frontend
# 选择 Vue 2 preset
cd frontend

5.2 创建 WebSocket 服务类 (src/utils/WebSocketService.js)

这是核心封装,实现自动重连与状态管理。

javascript 复制代码
class WebSocketService {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.reconnectInterval = 3000; // 重连间隔 3s
    this.maxReconnectAttempts = 10; // 最大重连次数
    this.reconnectAttempts = 0;
    this.heartbeatInterval = null;
    this.heartbeatTimeout = 45000; // 心跳超时时间(应大于服务端间隔)

    this.onOpen = () => {};
    this.onClose = () => {};
    this.onMessage = () => {};
    this.onError = () => {};
  }

  connect() {
    if (this.ws) {
      this.ws.close(); // 避免重复连接
    }

    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0; // 重置重连计数
      this.onOpen();
      this.startHeartbeat();
    };

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.onMessage(data);
      } catch (err) {
        console.error('Parse message error:', err);
        this.onError(err);
      }
    };

    this.ws.onclose = (event) => {
      console.log('WebSocket closed:', event.code, event.reason);
      this.stopHeartbeat();
      this.onClose(event);

      // 自动重连逻辑
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        console.log(`Reconnecting... attempt ${this.reconnectAttempts}`);
        setTimeout(() => {
          this.connect();
        }, this.reconnectInterval);
      } else {
        console.warn('Max reconnect attempts reached');
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.onError(error);
    };
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.warn('WebSocket is not open. ReadyState:', this.ws?.readyState);
    }
  }

  close() {
    if (this.ws) {
      this.ws.close();
      this.stopHeartbeat();
    }
  }

  startHeartbeat() {
    // 清除可能存在的旧定时器
    if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);

    // 每 25 秒发送一次心跳
    this.heartbeatInterval = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.send({ type: 'heartbeat', timestamp: Date.now() });
      }
    }, 25000);
  }

  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  // 设置回调函数(供 Vue 组件使用)
  setCallbacks({ onOpen, onClose, onMessage, onError }) {
    if (onOpen) this.onOpen = onOpen;
    if (onClose) this.onClose = onClose;
    if (onMessage) this.onMessage = onMessage;
    if (onError) this.onError = onError;
  }
}

// 单例模式导出
const socketService = new WebSocketService('ws://localhost:8080');
export default socketService;

5.3 创建 Vue 组件 (src/components/WebSocketClient.vue)

vue 复制代码
<template>
  <div class="websocket-client">
    <h2>WebSocket Client (Vue2)</h2>
    <p>状态: <span :class="statusClass">{{ status }}</span></p>
    <button @click="sendMessage" :disabled="status !== 'connected'">
      发送消息
    </button>
    <button @click="manualClose" v-if="status === 'connected'">
      手动断开
    </button>
    <button @click="reconnect" v-if="status === 'disconnected'">
      重连
    </button>

    <div class="messages">
      <h3>收到的消息:</h3>
      <div v-for="(msg, index) in messages" :key="index" class="message">
        [{{ msg.timestamp }}] {{ msg.data }}
      </div>
    </div>
  </div>
</template>

<script>
import socketService from '@/utils/WebSocketService';

export default {
  name: 'WebSocketClient',
  data() {
    return {
      status: 'connecting', // connecting, connected, disconnected, reconnecting
      messages: []
    };
  },
  computed: {
    statusClass() {
      return {
        'status-connecting': this.status === 'connecting',
        'status-connected': this.status === 'connected',
        'status-disconnected': this.status === 'disconnected',
        'status-reconnecting': this.status === 'reconnecting'
      };
    }
  },
  methods: {
    sendMessage() {
      const msg = `Hello from Vue2 at ${new Date().toLocaleTimeString()}`;
      socketService.send({ type: 'chat', data: msg });
    },
    manualClose() {
      socketService.close();
      this.status = 'disconnected';
    },
    reconnect() {
      this.status = 'connecting';
      socketService.connect();
    }
  },
  created() {
    // 设置 WebSocket 回调
    socketService.setCallbacks({
      onOpen: () => {
        this.status = 'connected';
      },
      onClose: (event) => {
        this.status = 'disconnected';
        if (socketService.reconnectAttempts > 0) {
          this.status = 'reconnecting';
        }
      },
      onMessage: (data) => {
        this.messages.push({
          data: data.data,
          timestamp: new Date().toLocaleTimeString()
        });
      },
      onError: (error) => {
        console.error('WebSocket error in component:', error);
      }
    });

    // 初始化连接
    socketService.connect();
  },
  beforeDestroy() {
    // 组件销毁时关闭连接
    socketService.close();
  }
};
</script>

<style scoped>
.websocket-client {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  max-width: 600px;
  margin: 20px auto;
}

.status-connecting { color: #ff9800; }
.status-connected { color: #4caf50; }
.status-disconnected { color: #f44336; }
.status-reconnecting { color: #ff5722; }

button {
  margin: 5px;
  padding: 8px 16px;
  cursor: pointer;
}

.messages {
  margin-top: 20px;
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid #eee;
  padding: 10px;
  border-radius: 4px;
}

.message {
  padding: 5px;
  border-bottom: 1px solid #eee;
  font-size: 14px;
}
</style>

5.4 在 App.vue 中使用组件

vue 复制代码
<template>
  <div id="app">
    <WebSocketClient />
  </div>
</template>

<script>
import WebSocketClient from './components/WebSocketClient.vue'

export default {
  name: 'App',
  components: {
    WebSocketClient
  }
}
</script>

六、运行项目

6.1 启动后端

bash 复制代码
cd backend
node server.js
# 输出:WebSocket server is running on ws://localhost:8080

6.2 启动前端

bash 复制代码
cd frontend
npm run serve
# 访问 http://localhost:8080

七、扩展与优化思路

7.1 安全性增强

  • WSS (WebSocket Secure) :使用 wss:// 替代 ws://,通过 HTTPS 传输。
  • 身份验证 :在 on('connection') 时校验 Token 或 Session。
  • 消息校验:对收到的消息进行 Schema 校验,防止恶意数据。

7.2 功能扩展

  • 房间/频道:支持用户加入不同频道,实现群聊。
  • 离线消息:结合数据库存储未送达消息。
  • 消息持久化:使用 Redis 或数据库记录聊天历史。
  • 服务端集群:使用 Redis Pub/Sub 实现多实例间消息同步。

7.3 性能优化

  • 心跳间隔动态调整:根据网络状况自适应心跳频率。
  • 消息压缩:对大消息使用 gzip 压缩。
  • 连接池管理:限制最大连接数,防止资源耗尽。

7.4 前端优化

  • 使用 Vuex 管理 WebSocket 状态:更适合复杂应用。
  • TypeScript 支持:提升代码健壮性。
  • WebSocket 封装为 Mixin 或 Composition API(Vue3)。

八、总结

本文通过一个完整的项目实践,详细展示了如何使用 Vue2 + Node.js + ws 构建一个具备心跳检测自动重连功能的 WebSocket 应用。我们从需求分析出发,合理选型,实现了前后端的完整通信逻辑,并封装了可复用的 WebSocket 服务类。

通过手动实现心跳与重连,我们深入理解了 WebSocket 的底层机制,避免了过度依赖第三方库,提升了系统的可控性与轻量化程度。

该项目可作为实时通信功能的起点,后续可根据实际业务需求进行扩展,如加入用户系统、消息加密、集群部署等。


参考资料

欢迎点赞、收藏、分享!如有疑问或建议,欢迎在评论区留言交流。

相关推荐
weixin_382395236 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__6 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.6 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~7 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华8 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子10 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅11 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni11 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学12 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端