You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在NextJS 13(app-router)中实现多会话实时WebSocket聊天的最优方案

基于NextJS 13 App Router + Express WebSocket实现多会话实时聊天

1. 搭建独立Express WebSocket服务器

这个方案完全独立于你现有的NextJS架构,几乎不用改动现有代码,只需新增一个轻量的Express服务来处理WebSocket连接。

步骤1:初始化服务并安装依赖

mkdir chat-ws-server && cd chat-ws-server
npm init -y
npm install express ws mongoose cors

步骤2:编写服务器核心代码

// server.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const mongoose = require('mongoose');
const cors = require('cors');

const app = express();
app.use(cors());
const server = http.createServer(app);

// 连接MongoDB(替换成你的数据库地址)
mongoose.connect('mongodb://localhost:27017/chat-app', {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).then(() => console.log('MongoDB已连接'))
  .catch(err => console.error('MongoDB连接失败:', err));

// 定义消息模型,和你现有数据库结构对齐
const MessageSchema = new mongoose.Schema({
  conversationId: String,
  senderId: String,
  content: String,
  timestamp: { type: Date, default: Date.now }
});
const Message = mongoose.model('Message', MessageSchema);

// WebSocket服务器配置
const wss = new WebSocket.Server({ server });

// 存储会话连接:key为conversationId,value为该会话的所有WebSocket连接
const conversationConnections = new Map();

wss.on('connection', (ws, req) => {
  // 从连接URL获取会话ID和用户身份信息
  const urlParams = new URLSearchParams(req.url.slice(1));
  const conversationId = urlParams.get('conversationId');
  const userId = urlParams.get('userId'); // 建议后续换成JWT验证,防止伪造

  // 校验必要参数
  if (!conversationId || !userId) {
    ws.close(4001, '缺少会话ID或用户ID');
    return;
  }

  // 将当前连接加入对应会话组
  if (!conversationConnections.has(conversationId)) {
    conversationConnections.set(conversationId, new Set());
  }
  conversationConnections.get(conversationId).add(ws);

  // 接收客户端消息并处理
  ws.on('message', async (data) => {
    try {
      const message = JSON.parse(data);
      // 验证消息合法性(确保用户只能操作自己的会话)
      if (message.senderId !== userId || message.conversationId !== conversationId) {
        ws.send(JSON.stringify({ error: '消息验证失败' }));
        return;
      }

      // 保存消息到MongoDB
      const newMessage = new Message(message);
      await newMessage.save();

      // 广播消息给会话内所有在线用户
      conversationConnections.get(conversationId).forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify(newMessage));
        }
      });
    } catch (err) {
      ws.send(JSON.stringify({ error: '消息处理失败' }));
    }
  });

  // 连接关闭时清理资源
  ws.on('close', () => {
    conversationConnections.get(conversationId)?.delete(ws);
    // 如果会话内没有连接了,清空该会话的存储
    if (conversationConnections.get(conversationId)?.size === 0) {
      conversationConnections.delete(conversationId);
    }
  });
});

const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`WebSocket服务器运行在端口 ${PORT}`);
});

2. NextJS客户端集成(App Router)

由于WebSocket需要浏览器环境,必须在客户端组件中实现连接逻辑。

步骤1:创建会话聊天组件

'use client';

import { useEffect, useRef, useState } from 'react';

export default function ChatConversation({ conversationId, userId }) {
  const wsRef = useRef(null);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    // 建立WebSocket连接,携带会话ID和用户ID
    const ws = new WebSocket(`ws://localhost:8080?conversationId=${conversationId}&userId=${userId}`);
    
    ws.onopen = () => {
      console.log(`已连接到会话 ${conversationId}`);
    };

    // 接收新消息并更新UI
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    ws.onclose = () => {
      console.log(`与会话 ${conversationId} 断开连接`);
      // 可选:添加断线重连逻辑
      // setTimeout(() => window.location.reload(), 3000);
    };

    wsRef.current = ws;

    // 组件卸载时关闭连接
    return () => {
      ws.close();
    };
  }, [conversationId, userId]);

  // 发送消息
  const sendMessage = () => {
    if (!input.trim() || !wsRef.current) return;

    const message = {
      conversationId,
      senderId: userId,
      content: input.trim()
    };

    wsRef.current.send(JSON.stringify(message));
    setInput('');
  };

  return (
    <div className="flex flex-col h-full max-w-md mx-auto">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map(msg => (
          <div 
            key={msg._id} 
            className={`flex ${msg.senderId === userId ? 'justify-end' : 'justify-start'}`}
          >
            <div 
              className={`px-4 py-2 rounded-lg ${
                msg.senderId === userId ? 'bg-blue-500 text-white' : 'bg-gray-200'
              }`}
            >
              <p>{msg.content}</p>
              <small className="text-xs opacity-70">
                {new Date(msg.timestamp).toLocaleTimeString()}
              </small>
            </div>
          </div>
        ))}
      </div>
      <div className="flex p-4 border-t">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="输入消息..."
          className="flex-1 px-4 py-2 rounded-l-lg border"
        />
        <button 
          onClick={sendMessage}
          className="px-6 py-2 bg-blue-500 text-white rounded-r-lg"
        >
          发送
        </button>
      </div>
    </div>
  );
}

步骤2:在会话页面中使用组件

// app/conversations/[id]/page.jsx
import ChatConversation from '@/app/components/ChatConversation';
import { getCurrentUser } from '@/lib/auth'; // 替换成你获取当前用户的方法
import { getConversationById } from '@/lib/mongodb'; // 从MongoDB获取会话详情

export default async function ConversationPage({ params }) {
  const user = await getCurrentUser();
  const conversation = await getConversationById(params.id);

  // 验证用户是否有权限访问该会话
  if (!conversation.participants.includes(user.id)) {
    return <div className="text-center mt-10">无访问权限</div>;
  }

  return (
    <div className="max-w-4xl mx-auto mt-8">
      <h1 className="text-2xl font-bold mb-4">会话:{conversation.title}</h1>
      <ChatConversation conversationId={params.id} userId={user.id} />
    </div>
  );
}

3. 关键优化与注意事项

  • 安全加固:不要直接传userId,改用JWT验证。客户端在连接时携带JWT,服务器解析后获取用户ID,防止身份伪造。
  • 断线重连:客户端可以添加自动重连逻辑,提升用户体验。
  • 资源清理:服务器会自动清理空会话的连接组,避免内存泄漏。
  • 部署适配:如果要同域名部署,用Nginx反向代理将/ws路径转发到WebSocket服务器,其他路径转发到NextJS,客户端连接地址可以改成相对路径/ws?conversationId=xxx&userId=xxx。
  • 历史消息加载:页面初始化时,从MongoDB加载该会话的历史消息,再结合WebSocket接收新消息。

4. 方案优势

  • 完全独立于现有NextJS App Router架构,几乎不需要改动现有代码。
  • 基于原生WebSocket实现,所有消息通过WebSocket通道收发,不依赖axios。
  • 每个会话对应独立的连接组,天然支持多会话同时在线,满足你的需求。

内容的提问来源于stack exchange,提问作者Fahad K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 17:07:49