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 的底层机制,避免了过度依赖第三方库,提升了系统的可控性与轻量化程度。

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


参考资料

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

相关推荐
狂炫一碗大米饭7 分钟前
Vue 3 的最佳开源分页库
前端·程序员·设计
你听得到1125 分钟前
告别重复造轮子!我从 0 到 1 封装一个搞定全场景的弹窗库!
前端·flutter·性能优化
Ali酱31 分钟前
2周斩获远程offer!我的高效求职秘诀全公开
前端·后端·面试
Cyanto1 小时前
Vue浅学
前端·javascript·vue.js
一只小风华~1 小时前
CSS aspect-ratio 属性
前端·css
Silver〄line1 小时前
以鼠标位置为中心进行滚动缩放
前端
LaiYoung_1 小时前
深入解析 single-spa 微前端框架核心原理
前端·javascript·面试
uhakadotcom2 小时前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试