从零搭建 Vue3 + Nest.js 实时通信项目:4 种方案(短轮询 / 长轮询 / SSE/WebSocket)

1. 短轮询(Short Polling):最简单的实时通信

核心原理

  • 客户端定期发送 HTTP 请求(如每 2 秒),获取服务器最新数据
  • 服务器收到请求后,立即返回当前数据(无论是否有更新)
  • 适合数据更新频率低的场景(如公告通知、非实时榜单)

后端实现(Nest.js)

backend/src/modules/polling/polling.controller.ts 中实现接口:

typescript

typescript 复制代码
import { Controller, Get } from '@nestjs/common';

@Controller('polling')
export class PollingController {
  // 模拟实时数据(如用户消息)
  private message = '初始消息:暂无新内容';
  private count = 0;

  // 短轮询接口:客户端定期调用
  @Get('short')
  getShortPollingData() {
    // 模拟随机更新数据(实际场景中从数据库/消息队列获取)
    if (Math.random() > 0.7) {
      this.count++;
      this.message = `新消息 ${this.count}:${new Date().toLocaleTimeString()}`;
    }

    // 立即返回当前数据
    return {
      code: 200,
      data: { message: this.message },
      timestamp: Date.now(),
    };
  }
}

前端实现(Vue3)

frontend/src/views/ShortPolling.vue 中实现定期请求逻辑:

xml 复制代码
<template>
  <div class="short-polling">
    <h2>短轮询方案</h2>
    <p>请求频率:2秒/次</p>
    <div class="message-box">
      <h3>最新消息:</h3>
      <p>{{ message }}</p>
    </div>
    <button @click="togglePolling">{{ isPolling ? '停止轮询' : '开始轮询' }}</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import axios from 'axios';

const message = ref('初始消息:暂无新内容');
const isPolling = ref(true);
let timer: NodeJS.Timeout;

// 发起短轮询请求
const fetchShortPollingData = async () => {
  try {
    const res = await axios.get('http://localhost:3000/polling/short');
    if (res.data.code === 200) {
      message.value = res.data.data.message; // 更新页面数据
    }
  } catch (err) {
    console.error('短轮询请求失败:', err);
  }
};

// 启动/停止轮询
const togglePolling = () => {
  isPolling.value = !isPolling.value;
  if (isPolling.value) {
    fetchShortPollingData(); // 立即请求一次
    timer = setInterval(fetchShortPollingData, 2000); // 每2秒请求一次
  } else {
    clearInterval(timer); // 清除定时器
  }
};

// 组件挂载时启动轮询
togglePolling();

// 组件卸载时清除定时器(避免内存泄漏)
onUnmounted(() => {
  clearInterval(timer);
});
</script>

<style scoped>
.short-polling { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>

优缺点对比

优点 缺点
实现简单,无需特殊协议 频繁发送请求,浪费带宽和服务器资源
兼容性好(所有浏览器支持) 实时性差(延迟取决于轮询间隔)

2. 长轮询(Long Polling):优化版轮询

核心原理

  • 客户端发送 HTTP 请求后,服务器保持连接不返回,直到有新数据或超时
  • 服务器返回数据后,客户端立即发送下一次请求,形成「长连接循环」
  • 相比短轮询,减少了请求次数,适合数据更新频率中等的场景(如 IM 聊天、实时通知)

后端实现(Nest.js)

backend/src/modules/polling/polling.controller.ts 中添加长轮询接口:

typescript 复制代码
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { Observable, Subject, timeout } from 'rxjs';

@Controller('polling')
export class PollingController {
  private longPollingSubject = new Subject<string>(); // 用于触发数据更新
  private message = '初始消息:暂无新内容';
  private count = 0;

  // 模拟外部数据更新(如数据库变更、其他服务推送)
  constructor() {
    setInterval(() => {
      if (Math.random() > 0.5) {
        this.count++;
        this.message = `长轮询新消息 ${this.count}:${new Date().toLocaleTimeString()}`;
        this.longPollingSubject.next(this.message); // 触发长连接返回
      }
    }, 3000);
  }

  // 长轮询接口:保持连接直到有新数据或超时
  @Get('long')
  getLongPollingData(): Observable<{ code: number; data: { message: string }; timestamp: number }> {
    return new Observable((observer) => {
      // 订阅数据更新事件
      const subscription = this.longPollingSubject.subscribe((newMessage) => {
        observer.next({
          code: 200,
          data: { message: newMessage },
          timestamp: Date.now(),
        });
        observer.complete(); // 返回数据后关闭连接
        subscription.unsubscribe(); // 取消订阅
      });

      // 超时处理(5秒无数据则返回空,避免连接一直挂着)
      setTimeout(() => {
        observer.next({
          code: HttpStatus.NO_CONTENT, // 204 无内容
          data: { message: this.message },
          timestamp: Date.now(),
        });
        observer.complete();
        subscription.unsubscribe();
      }, 5000);
    });
  }
}

前端实现(Vue3)

frontend/src/views/LongPolling.vue 中实现长轮询循环:

xml 复制代码
<template>
  <div class="long-polling">
    <h2>长轮询方案</h2>
    <p>机制:服务器保持连接,有新数据才返回</p>
    <div class="message-box">
      <h3>最新消息:</h3>
      <p>{{ message }}</p>
    </div>
    <button @click="togglePolling">{{ isPolling ? '停止轮询' : '开始轮询' }}</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import axios from 'axios';

const message = ref('初始消息:暂无新内容');
const isPolling = ref(true);

// 发起长轮询请求(递归调用,形成循环)
const fetchLongPollingData = async () => {
  if (!isPolling.value) return; // 停止轮询则退出

  try {
    const res = await axios.get('http://localhost:3000/polling/long', {
      timeout: 6000, // 超时时间需大于后端超时(5秒)
    });

    if (res.data.code === 200) {
      message.value = res.data.data.message; // 有新数据则更新
    }
    // 无论是否有新数据,立即发起下一次请求
    setTimeout(fetchLongPollingData, 0);
  } catch (err) {
    console.error('长轮询请求失败:', err);
    // 出错后延迟重试(避免频繁报错)
    setTimeout(fetchLongPollingData, 1000);
  }
};

// 启动/停止长轮询
const togglePolling = () => {
  isPolling.value = !isPolling.value;
  if (isPolling.value) {
    fetchLongPollingData(); // 启动循环
  }
};

// 组件挂载时启动
togglePolling();

// 组件卸载时停止
onUnmounted(() => {
  isPolling.value = false;
});
</script>

<style scoped>
/* 样式与短轮询类似,可复用 */
.long-polling { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>

优缺点对比

优点 缺点
减少请求次数,节省资源 服务器需保持连接,占用内存
实时性比短轮询好 超时控制复杂,可能出现连接堆积

3. 服务器发送事件(SSE):单向实时推送

核心原理

  • 基于 HTTP 协议,建立「客户端 → 服务器」的单向长连接
  • 服务器通过该连接持续向客户端推送数据(支持文本、JSON 等格式)
  • 适合服务器单向推送场景(如实时日志、数据看板、股票行情)

后端实现(Nest.js)

backend/src/modules/sse/sse.controller.ts 中实现 SSE 接口:

typescript 复制代码
import { Controller, Get, Res, Sse } from '@nestjs/common';
import { Response } from 'express';
import { Observable, interval, map } from 'rxjs';

@Controller('sse')
export class SseController {
  // 方案1:使用 Nest.js 内置 Sse 装饰器(推荐)
  @Sse('data')
  getSseData(): Observable<MessageEvent> {
    // 每2秒推送一次数据(模拟实时更新)
    return interval(2000).pipe(
      map((count) => {
        const message = `SSE 推送数据 ${count + 1}:${new Date().toLocaleTimeString()}`;
        return {
          type: 'message', // 事件类型(客户端可按类型监听)
          data: { message }, // 推送数据
          id: count.toString(), // 事件ID
        } as MessageEvent;
      }),
    );
  }

  // 方案2:手动处理 SSE 响应头(适合自定义场景)
  @Get('custom')
  getCustomSse(@Res() res: Response) {
    // 设置 SSE 响应头(必须)
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    // 每3秒推送一次数据
    let count = 0;
    const timer = setInterval(() => {
      count++;
      const message = `自定义 SSE 数据 ${count}:${new Date().toLocaleTimeString()}`;
      // SSE 数据格式:data: {JSON}\n\n(必须以\n\n结尾)
      res.write(`data: ${JSON.stringify({ message })}\n\n`);

      // 模拟客户端断开连接,清理定时器
      res.on('close', () => {
        clearInterval(timer);
        res.end();
      });
    }, 3000);
  }
}

前端实现(Vue3)

frontend/src/views/SSE.vue 中监听服务器推送:

xml 复制代码
<template>
  <div class="sse">
    <h2>服务器发送事件(SSE)</h2>
    <p>机制:服务器单向推送,客户端被动接收</p>
    <div class="message-box">
      <h3>推送历史:</h3>
      <ul>
        <li v-for="(item, index) in messageList" :key="index">{{ item }}</li>
      </ul>
    </div>
    <button @click="toggleSSE">{{ isConnected ? '断开连接' : '建立连接' }}</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue';

const messageList = ref<string[]>(['等待服务器推送...']);
const isConnected = ref(false);
let eventSource: EventSource | null = null;

// 建立/断开 SSE 连接
const toggleSSE = () => {
  if (isConnected.value) {
    // 断开连接
    eventSource?.close();
    eventSource = null;
    isConnected.value = false;
    messageList.value.push('已断开 SSE 连接');
  } else {
    // 建立连接(使用 Nest.js 内置 SSE 接口)
    eventSource = new EventSource('http://localhost:3000/sse/data');
    
    // 监听正常推送(type: 'message')
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      messageList.value.push(data.message);
      // 只保留最近10条记录,避免页面卡顿
      if (messageList.value.length > 10) {
        messageList.value.shift();
      }
    };

    // 监听自定义事件类型(可选)
    eventSource.addEventListener('customEvent', (event) => {
      messageList.value.push(`自定义事件:${event.data}`);
    });

    // 监听连接错误
    eventSource.onerror = (error) => {
      console.error('SSE 连接错误:', error);
      messageList.value.push('SSE 连接异常,已断开');
      isConnected.value = false;
      eventSource.close();
    };

    // 监听连接成功
    eventSource.onopen = () => {
      isConnected.value = true;
      messageList.value.push('已建立 SSE 连接');
    };
  }
};

// 组件卸载时断开连接
onUnmounted(() => {
  eventSource?.close();
});
</script>

<style scoped>
.sse { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; max-height: 300px; overflow-y: auto; }
ul { list-style: disc; margin-left: 20px; }
li { margin: 5px 0; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>

优缺点对比

优点 缺点
单向推送,节省客户端资源 仅支持服务器 → 客户端单向通信
自动重连(浏览器原生支持) 单个连接只能推送文本数据(需序列化)
轻量级,无需复杂协议 部分代理服务器可能不支持长连接

4. WebSocket:全双工实时通信

核心原理

  • 基于 TCP 协议,建立「客户端 ↔ 服务器」的全双工长连接
  • 连接建立后,双方可随时发送数据(无需重复建立连接)
  • 实时性最好,适合频繁双向通信场景(如 IM 聊天、在线协作、游戏)

后端实现(Nest.js)

使用 @nestjs/websocketssocket.io 实现 WebSocket 服务:

  1. 首先在 backend/src/app.module.ts 中导入 WebSocket 模块:

typescript

typescript 复制代码
import { Module } from '@nestjs/common';
import { WebSocketModule } from './modules/websocket/websocket.module';

@Module({
  imports: [
    WebSocketModule,
    // 其他模块...
  ],
})
export class AppModule {}
  1. backend/src/modules/websocket/websocket.gateway.ts 中实现网关:

typescript

typescript 复制代码
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

// 配置跨域(允许前端访问)
@WebSocketGateway({
  cors: {
    origin: 'http://localhost:5173', // 前端地址
    methods: ['GET', 'POST'],
    credentials: true,
  },
  port: 3001, // WebSocket 独立端口(避免与 HTTP 端口冲突)
})
export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server; // 全局服务器实例
  private onlineUsers = 0; // 在线用户数

  // 客户端连接时触发
  handleConnection(client: Socket) {
    this.onlineUsers++;
    console.log(`客户端连接:${client.id},当前在线:${this.onlineUsers}`);
    
    // 向新连接的客户端发送欢迎消息
    client.emit('welcome', { message: `欢迎连接 WebSocket,你的ID:${client.id}` });
    
    // 向所有客户端广播在线人数
    this.server.emit('onlineCount', { count: this.onlineUsers });
  }

  // 客户端断开时触发
  handleDisconnect(client: Socket) {
    this.onlineUsers--;
    console.log(`客户端断开:${client.id},当前在线:${this.onlineUsers}`);
    
    // 广播在线人数更新
    this.server.emit('onlineCount', { count: this.onlineUsers });
  }

  // 监听客户端发送的「sendMessage」事件
  @SubscribeMessage('sendMessage')
  handleSendMessage(client: Socket, data: { content: string }) {
    console.log(`收到 ${client.id} 的消息:${data.content}`);
    
    // 方案1:向所有客户端广播消息(含发送者)
    this.server.emit('newMessage', {
      from: client.id,
      content: data.content,
      time: new Date().toLocaleTimeString(),
    });

    // 方案2:仅向发送者以外的客户端广播(可选)
    // client.broadcast.emit('newMessage', { from: client.id, content: data.content });
  }

  // 模拟服务器主动推送(如定时推送)
  constructor() {
    setInterval(() => {
      this.server.emit('serverPush', {
        message: `服务器定时推送:${new Date().toLocaleTimeString()}`,
      });
    }, 5000);
  }
}
  1. backend/src/modules/websocket/websocket.module.ts 中导出模块:

typescript

python 复制代码
import { Module } from '@nestjs/common';
import { WebSocketGateway } from './websocket.gateway';

@Module({
  providers: [WebSocketGateway],
})
export class WebSocketModule {}

前端实现(Vue3)

  1. frontend/src/views/WebSocket.vue 中实现客户端逻辑:
xml 复制代码
<template>
  <div class="websocket">
    <h2>WebSocket 全双工通信</h2>
    <p>机制:客户端 ↔ 服务器双向实时通信</p>
    <div class="status">
      <p>连接状态:{{ isConnected ? '✅ 已连接' : '❌ 未连接' }}</p>
      <p>在线人数:{{ onlineCount }}</p>
    </div>

    <!-- 消息列表 -->
    <div class="message-list">
      <h3>消息记录:</h3>
      <div v-for="(msg, index) in messageList" :key="index" class="message-item">
        <span class="time">{{ msg.time }}</span>
        <span class="from">{{ msg.from }}:</span>
        <span class="content">{{ msg.content }}</span>
      </div>
    </div>

    <!-- 发送消息表单 -->
    <div class="send-form">
      <input
        v-model="inputContent"
        placeholder="输入消息..."
        @keyup.enter="sendMessage"
      />
      <button @click="sendMessage" :disabled="!isConnected">发送</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import { io } from 'socket.io-client';

const isConnected = ref(false);
const onlineCount = ref(0);
const inputContent = ref('');
const messageList = ref<Array<{ from: string; content: string; time: string }>>([]);
let socket: ReturnType<typeof io> | null = null;

// 连接 WebSocket
const connectWebSocket = () => {
  // 连接后端 WebSocket 服务(端口 3001)
  socket = io('http://localhost:3001');

  // 连接成功
  socket.on('connect', () => {
    isConnected.value = true;
    messageList.value.push({
      from: '系统',
      content: '已成功连接 WebSocket',
      time: new Date().toLocaleTimeString(),
    });
  });

  // 连接失败
  socket.on('connect_error', (error) => {
    console.error('WebSocket 连接失败:', error);
    isConnected.value = false;
    messageList.value.push({
      from: '系统',
      content: 'WebSocket 连接失败,请重试',
      time: new Date().toLocaleTimeString(),
    });
  });

  // 断开连接
  socket.on('disconnect', () => {
    isConnected.value = false;
    messageList.value.push({
      from: '系统',
      content: 'WebSocket 已断开连接',
      time: new Date().toLocaleTimeString(),
    });
  });

  // 监听欢迎消息
  socket.on('welcome', (data) => {
    messageList.value.push({
      from: '系统',
      content: data.message,
      time: new Date().toLocaleTimeString(),
    });
  });

  // 监听在线人数更新
  socket.on('onlineCount', (data) => {
    onlineCount.value = data.count;
  });

  // 监听新消息
  socket.on('newMessage', (data) => {
    messageList.value.push(data);
    // 滚动到最新消息
    const messageListEl = document.querySelector('.message-list');
    if (messageListEl) {
      messageListEl.scrollTop = messageListEl.scrollHeight;
    }
  });

  // 监听服务器定时推送
  socket.on('serverPush', (data) => {
    messageList.value.push({
      from: '服务器',
      content: data.message,
      time: new Date().toLocaleTimeString(),
    });
  });
};

// 发送消息
const sendMessage = () => {
  if (!inputContent.value.trim() || !isConnected.value) return;
  
  // 向服务器发送「sendMessage」事件
  socket?.emit('sendMessage', { content: inputContent.value.trim() });
  
  // 清空输入框
  inputContent.value = '';
};

// 组件挂载时连接
connectWebSocket();

// 组件卸载时断开连接
onUnmounted(() => {
  socket?.disconnect();
});
</script>

<style scoped>
.websocket { padding: 20px; }
.status { margin: 10px 0; color: #666; }
.message-list { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; max-height: 300px; overflow-y: auto; }
.message-item { margin: 8px 0; padding: 5px; border-bottom: 1px dashed #f5f5f5; }
.time { color: #999; font-size: 12px; margin-right: 10px; }
.from { font-weight: bold; margin-right: 10px; }
.send-form { display: flex; gap: 10px; }
input { flex: 1; padding: 8px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:disabled { background: #ccc; cursor: not-allowed; }
</style>

优缺点对比

优点 缺点
全双工通信,实时性最好 实现复杂度高,需处理连接状态
减少连接次数,节省资源 部分老旧浏览器不支持(需降级方案)
支持二进制数据传输 服务器需维护长连接,占用内存较多

四、四种方案对比与选型建议

通过前面的实现,我们可以清晰看到四种方案的差异,实际项目中需根据业务场景选择:

对比维度 短轮询 长轮询 SSE WebSocket
通信方向 客户端 → 服务器(单向) 客户端 → 服务器(单向) 服务器 → 客户端(单向) 双向
实时性 差(取决于轮询间隔) 最好
服务器资源占用 高(频繁请求) 中(保持连接) 中(单向长连接) 低(单连接复用)
实现复杂度 极低
兼容性 所有浏览器支持 所有浏览器支持 IE 不支持 IE 10+ 支持
适用场景 数据更新频率低(如公告) 数据更新频率中等(如通知) 服务器单向推送(如日志) 频繁双向通信(如 IM)

选型建议

  1. 快速验证需求:优先用短轮询(实现快,无需复杂配置)
  2. 通知类场景:用长轮询或 SSE(减少请求,节省资源)
  3. 实时看板 / 日志:用 SSE(单向推送,客户端无需发送数据)
  4. IM / 在线协作:必须用 WebSocket(全双工通信,实时性要求高)
  5. 兼容性要求高:短轮询 + WebSocket 降级(老浏览器用短轮询,新浏览器用 WebSocket)

git地址:gitee.com/xcxsj/realt...

相关推荐
LaoZhangAI2 小时前
Google Gemini Nano与Banana AI完整部署指南:2025年轻量级AI解决方案
前端·后端
用户11481867894842 小时前
基于 Webpack Module Federation 的 Vue 微前端实践
前端
Java水解2 小时前
spring中的@SpringBootTest注解详解
spring boot·后端
怪可爱的地球人2 小时前
Pinia状态管理有哪些常用API?
前端
小高0072 小时前
🤔函数柯里化:化繁为简的艺术与实践
前端·javascript·面试
似水流年流不尽思念2 小时前
Java线程状态转换的详细过程
后端
却尘2 小时前
React useMemo 依赖陷阱:组件重挂载,状态无限复原
前端·javascript·react.js
尚学教辅学习资料2 小时前
基于Spring Boot的家政服务管理系统+论文示例参考
java·spring boot·后端·java毕设
Java水解2 小时前
从 “Hello AI” 到企业级应用:Spring AI 如何重塑 Java 生态的 AI 开发
后端·spring