文章目录
-
- 在当今数字化时代,实时在线聊天功能已成为网站和应用程序的标配功能。无论是电商平台的客服咨询、社交网站的即时通讯,还是企业内部协作工具,实时聊天功能都能显著提升用户体验和沟通效率。本教程将详细介绍如何为您的网站集成一个完整的实时在线聊天与通讯应用,包括前端界面、后端服务和数据库设计。
- 在开始开发之前,我们需要选择合适的技术栈: 前端:HTML5、CSS3、JavaScript(使用原生JS或框架如React/Vue) 后端:Node.js + Express.js 实时通信:Socket.IO(基于WebSocket的库) 数据库:MongoDB(存储用户信息和聊天记录) 部署:可以使用Heroku、AWS或Vercel等云服务
- chat-application/ ├── public/ │ ├── index.html │ ├── css/ │ │ └── style.css │ └── js/ │ └── client.js ├── server/ │ ├── server.js │ ├── models/ │ │ └── Message.js │ └── routes/ │ └── api.js ├── package.json └── README.md
- 首先,我们需要创建一个基础的Express服务器,并集成Socket.IO用于实时通信。 // server/server.js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const mongoose = require('mongoose'); const cors = require('cors'); // 初始化Express应用 const app = express(); const server = http.createServer(app); // 配置Socket.IO const io = socketIo(server, { cors: { origin: "http://localhost:3000", // 前端地址 methods: ["GET", "POST"] } }); // 中间件配置 app.use(cors()); app.use(express.json()); app.use(express.static('public')); // 静态文件服务 // 连接MongoDB数据库 const mongoURI = 'mongodb://localhost:27017/chat_app'; mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => console.log('MongoDB连接成功')) .catch(err => console.log('MongoDB连接失败:', err)); // 定义消息模型 const Message = require('./models/Message'); // Socket.IO连接处理 io.on('connection', (socket) => { console.log('新用户连接:', socket.id); // 用户加入聊天室 socket.on('join', (username) => { socket.username = username; socket.join('general'); // 加入默认聊天室 console.log(`${username}加入了聊天室`); // 通知其他用户 socket.to('general').emit('user_joined', { username: username, time: new Date() }); // 发送欢迎消息 socket.emit('message', { username: '系统', text: `欢迎 ${username} 加入聊天室!`, time: new Date() }); }); // 处理聊天消息 socket.on('chat_message', async (data) => { const message = new Message({ username: data.username, text: data.text, room: 'general', time: new Date() }); try { // 保存消息到数据库 await message.save(); // 广播消息给所有用户 io.to('general').emit('message', { username: data.username, text: data.text, time: new Date() }); } catch (error) { console.error('保存消息失败:', error); } }); // 用户断开连接 socket.on('disconnect', () => { if (socket.username) { console.log(`${socket.username}断开连接`); socket.to('general').emit('user_left', { username: socket.username, time: new Date() }); } }); }); // 启动服务器 const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`服务器运行在 http://localhost:${PORT}`); });
- 接下来,我们需要定义MongoDB数据模型来存储聊天消息。 // server/models/Message.js const mongoose = require('mongoose'); const messageSchema = new mongoose.Schema({ username: { type: String, required: true, trim: true }, text: { type: String, required: true, trim: true, maxlength: 500 }, room: { type: String, default: 'general', trim: true }, time: { type: Date, default: Date.now } }); // 创建索引以提高查询效率 messageSchema.index({ room: 1, time: -1 }); module.exports = mongoose.model('Message', messageSchema);
- 现在让我们创建一个美观且功能完整的聊天界面。 <!-- public/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>实时在线聊天应用</title> <link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> </head> <body> <div class="container"> <!-- 登录界面 --> <div id="login-container" class="login-container"> <div class="login-box"> <h1><i class="fas fa-comments"></i> 实时聊天室</h1> <p>请输入用户名加入聊天</p> <div class="input-group"> <input type="text" id="username" placeholder="请输入用户名" maxlength="20"> <button id="join-btn" class="btn-primary"> <i class="fas fa-sign-in-alt"></i> 加入聊天 </button> </div> <div class="room-selection"> <label for="room-select">选择聊天室:</label> <select id="room-select"> <option value="general">综合聊天室</option> <option value="tech">技术讨论</option> <option value="games">游戏交流</option> <option value="random">随机闲聊</option> </select> </div> </div> </div> <!-- 聊天主界面 --> <div id="chat-container" class="chat-container hidden"> <div class="chat-header"> <h2><i class="fas fa-comment-dots"></i> 实时聊天室</h2> <div class="user-info"> <span id="current-user"></span> <span id="current-room" class="room-badge"></span> <button id="leave-btn" class="btn-secondary"> <i class="fas fa-sign-out-alt"></i> 退出 </button> </div> </div> <div class="chat-main"> <!-- 在线用户列表 --> <div class="sidebar"> <h3><i class="fas fa-users"></i> 在线用户 (<span id="user-count">0</span>)</h3> <ul id="user-list"></ul> <div class="room-list"> <h4><i class="fas fa-door-open"></i> 聊天室</h4> <ul id="room-list"> <li class="active" data-room="general">综合聊天室</li> <li data-room="tech">技术讨论</li> <li data-room="games">游戏交流</li> <li data-room="random">随机闲聊</li> </ul> </div> </div> <!-- 聊天消息区域 --> <div class="chat-area"> <div id="messages" class="messages"> <!-- 消息将通过JavaScript动态添加 --> </div> <!-- 消息输入区域 --> <div class="message-input"> <div class="input-group"> <input type="text" id="message-input" placeholder="输入消息..." maxlength="500"> <button id="send-btn" class="btn-primary"> <i class="fas fa-paper-plane"></i> 发送 </button> </div> <div class="input-actions"> <button id="emoji-btn" class="btn-icon" title="表情"> <i class="far fa-smile"></i> </button> <button id="upload-btn" class="btn-icon" title="上传文件"> <i class="fas fa-paperclip"></i> </button> <span id="char-count">0/500</span> </div> </div> </div> </div> </div> </div> <!-- Socket.IO客户端库 --> <script src="/socket.io/socket.io.js"></script> <!-- 自定义JavaScript --> <script src="js/client.js"></script> </body> </html>
- 让我们为聊天应用添加现代化的样式。 /* public/css/style.css */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .container { width: 100%; max-width: 1200px; height: 90vh; background-color: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2); overflow: hidden; } /* 登录界面样式 */ .login-container { display: flex; justify-content: center; align-items: center; height: 100%; padding: 20px; } .login-box { background: white; padding: 40px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); text-align: center; width: 100%; max-width: 500px; } .login-box h1 { color: #2575fc; margin-bottom: 10px; font-size: 2.5rem; } .login-box p { color: #666; margin-bottom: 30px; font-size: 1.1rem; } .input-group { display: flex; margin-bottom: 20px; } .input-group input, .input-group select { flex: 1; padding: 15px; border: 2px solid #e1e5eb; border-radius: 10px 0 0 10px; font-size: 1rem; outline: none; transition: border-color 0.3s; } .input-group input:focus, .input-group select:focus { border-color: #2575fc; } .btn-primary { background: linear-gradient(to right, #6a11cb, #2575fc); color: white; border: none; padding: 15px 25px; border-radius: 0 10px 10px 0; cursor: pointer; font-size: 1rem; font-weight: 600; transition: all 0.3s; } .btn-primary:hover { opacity: 0.9; transform: translateY(-2px); } .room-selection { text-align: left; margin-top: 20px; } .room-selection label { display: block; margin-bottom: 8px; color: #555; font-weight: 500; } .room-selection select { width: 100%; padding: 12px; border-radius: 10px; border: 2px solid #e1e5eb; } /* 聊天界面样式 */ .chat-container { height: 100%; display: flex; flex-direction: column; } .chat-header { background: linear-gradient(to right, #6a11cb, #2575fc); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; } .chat-header h2 { font-size: 1.8rem; } .user-info { display: flex; align-items: center; gap: 15px; } .room-badge { background: rgba(255, 255, 255, 0.2); padding: 5px 15px; border-radius: 20px; font-size: 0.9rem; } .btn-secondary { background: transparent; color: white; border: 2px solid rgba(255, 255, 255, 0.5); padding: 8px 15px; border-radius: 10px; cursor: pointer; transition: all 0.3s; } .btn-secondary:hover { background: rgba(255, 255, 255, 0.1); } .chat-main { display: flex; flex: 1; overflow: hidden; } .sidebar { width: 250px; background: #f8f9fa; border-right: 1px solid #e1e5eb; padding: 20px; overflow-y: auto; } .sidebar h3, .sidebar h4 { color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #e1e5eb; } #user-list { list-style: none; margin-bottom: 30px; } #user-list li { padding: 10px 15px; margin-bottom: 8px; background: white; border-radius: 10px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } #user-list li:before { content: "•"; color: #4CAF50; font-size: 2rem; margin-right: 10px; } .room-list ul { list-style: none; } .room-list li { padding: 12px 15px; margin-bottom: 8px; background: white; border-radius: 10px; cursor: pointer; transition: all 0.3s; } .room-list li:hover { background: #eef5ff; } .room-list li.active { background: #2575fc; color: white; font-weight: 600; } .chat-area { flex: 1; display: flex; flex-direction: column; padding: 20px; } .messages { flex: 1; overflow-y: auto; padding: 20px; background: white; border-radius: 15px; margin-bottom: 20px; box-shadow: inset 0 0 10px rgba(0,0,0,0.05); } .message { margin-bottom: 20px; padding: 15px; border-radius: 15px; max-width: 70%; word-wrap: break-word; } .message.system { background: #f0f0f0; color: #666; text-align: center; max-width: 100%; font-style: italic; } .message.received { background: #f1f3f4; align-self: flex-start; border-bottom-left-radius: 5px; } .message.sent { background: linear-gradient(to right, #6a11cb, #2575fc); color: white; align-self: flex-end; border-bottom-right-radius: 5px; } .message-header { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 0.9rem; } .message-sender { font-weight: 600; } .message-time { opacity: 0.7; } .message-text { line-height: 1.5; } .message-input { background: white; padding: 20px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.05); } .input-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; } .btn-icon { background: none; border: none; color: #666; font-size: 1.2rem; cursor: pointer; padding: 5px 10px; border-radius: 5px; transition: all 0.3s; } .btn-icon:hover { background: #f0f0f0; color: #2575fc; } #char-count { color: #999; font-size: 0.9rem; } .hidden { display: none !important; } /* 响应式设计 */ @media (max-width: 768px) { .chat-main { flex-direction: column; } .sidebar { width: 100%; height: 200px; border-right: none; border-bottom: 1px solid #e1e5eb; } .message { max-width: 85%; } .chat-header { flex-direction: column; gap: 15px; text-align: center; } }
- 最后,我们需要编写客户端JavaScript代码来处理用户交互和Socket.IO通信。 // public/js/client.js document.addEventListener('DOMContentLoaded', function() { // 获取DOM元素 // public/js/client.js - 续接上文 const chatContainer = document.getElementById('chat-container'); const usernameInput = document.getElementById('username'); const joinBtn = document.getElementById('join-btn'); const leaveBtn = document.getElementById('leave-btn'); const messageInput = document.getElementById('message-input'); const sendBtn = document.getElementById('send-btn'); const messagesContainer = document.getElementById('messages'); const userList = document.getElementById('user-list'); const userCount = document.getElementById('user-count'); const currentUserSpan = document.getElementById('current-user'); const currentRoomSpan = document.getElementById('current-room'); const roomSelect = document.getElementById('room-select'); const roomListItems = document.querySelectorAll('#room-list li'); const charCount = document.getElementById('char-count'); const emojiBtn = document.getElementById('emoji-btn'); // 初始化变量 let socket; let currentUser = ''; let currentRoom = 'general'; let onlineUsers = new Set(); // 连接Socket.IO服务器 function connectToServer() { // 连接到后端服务器 socket = io('http://localhost:3000'); // 连接成功 socket.on('connect', () => { console.log('已连接到服务器'); }); // 接收消息 socket.on('message', (data) => { addMessage(data.username, data.text, data.time, data.username === currentUser ? 'sent' : 'received'); }); // 用户加入通知 socket.on('user_joined', (data) => { addSystemMessage(`${data.username} 加入了聊天室`, data.time); addUserToList(data.username); }); // 用户离开通知 socket.on('user_left', (data) => { addSystemMessage(`${data.username} 离开了聊天室`, data.time); removeUserFromList(data.username); }); // 错误处理 socket.on('connect_error', (error) => { console.error('连接错误:', error); alert('无法连接到服务器,请检查网络连接'); }); } // 添加消息到聊天界面 function addMessage(username, text, time, type = 'received') { const messageDiv = document.createElement('div'); messageDiv.className = `message ${type}`; const timeString = new Date(time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageDiv.innerHTML = ` <div class="message-header"> <span class="message-sender">${username}</span> <span class="message-time">${timeString}</span> </div> <div class="message-text">${escapeHtml(text)}</div> `; messagesContainer.appendChild(messageDiv); scrollToBottom(); } // 添加系统消息 function addSystemMessage(text, time) { const messageDiv = document.createElement('div'); messageDiv.className = 'message system'; const timeString = new Date(time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageDiv.innerHTML = ` <div class="message-text"> <i class="fas fa-info-circle"></i> ${escapeHtml(text)} <span class="message-time">${timeString}</span> </div> `; messagesContainer.appendChild(messageDiv); scrollToBottom(); } // 添加用户到在线列表 function addUserToList(username) { if (onlineUsers.has(username)) return; onlineUsers.add(username); updateUserList(); } // 从在线列表移除用户 function removeUserFromList(username) { onlineUsers.delete(username); updateUserList(); } // 更新在线用户列表 function updateUserList() { userList.innerHTML = ''; onlineUsers.forEach(user => { const li = document.createElement('li'); li.textContent = user; userList.appendChild(li); }); userCount.textContent = onlineUsers.size; } // 滚动到消息底部 function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } // HTML转义防止XSS攻击 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 加入聊天室 function joinChat() { const username = usernameInput.value.trim(); if (!username) { alert('请输入用户名'); usernameInput.focus(); return; } if (username.length > 20) { alert('用户名不能超过20个字符'); return; } currentUser = username; currentRoom = roomSelect.value; // 连接服务器 connectToServer(); // 发送加入请求 socket.emit('join', currentUser); // 更新界面 currentUserSpan.textContent = currentUser; currentRoomSpan.textContent = currentRoom; // 切换到聊天界面 loginContainer.classList.add('hidden'); chatContainer.classList.remove('hidden'); // 添加自己到用户列表 addUserToList(currentUser); // 聚焦到消息输入框 messageInput.focus(); } // 发送消息 function sendMessage() { const text = messageInput.value.trim(); if (!text) { alert('请输入消息内容'); return; } if (text.length > 500) { alert('消息不能超过500个字符'); return; } // 发送消息到服务器 socket.emit('chat_message', { username: currentUser, text: text, room: currentRoom }); // 清空输入框 messageInput.value = ''; updateCharCount(); } // 离开聊天室 function leaveChat() { if (socket) { socket.disconnect(); } // 重置状态 currentUser = ''; onlineUsers.clear(); messagesContainer.innerHTML = ''; userList.innerHTML = ''; usernameInput.value = ''; messageInput.value = ''; // 切换回登录界面 chatContainer.classList.add('hidden'); loginContainer.classList.remove('hidden'); } // 切换聊天室 function switchRoom(roomName) { if (roomName === currentRoom) return; // 更新当前房间 currentRoom = roomName; currentRoomSpan.textContent = roomName; // 更新房间选择状态 roomListItems.forEach(item => { if (item.dataset.room === roomName) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // 清空当前消息 messagesContainer.innerHTML = ''; // 发送房间切换请求到服务器 socket.emit('switch_room', { username: currentUser, oldRoom: currentRoom, newRoom: roomName }); // 添加系统消息 addSystemMessage(`您已切换到 ${roomName} 聊天室`, new Date()); } // 更新字符计数 function updateCharCount() { const length = messageInput.value.length; charCount.textContent = `${length}/500`; if (length > 450) { charCount.style.color = '#ff6b6b'; } else if (length > 400) { charCount.style.color = '#ffa726'; } else { charCount.style.color = '#999'; } } // 初始化表情选择器 function initEmojiPicker() { // 简单的表情列表 const emojis = ['😀', '😂', '😊', '😍', '👍', '👋', '🎉', '🔥', '❤️', '✨']; emojiBtn.addEventListener('click', () => { // 创建表情选择器 const picker = document.createElement('div'); picker.className = 'emoji-picker'; picker.style.cssText = ` position: absolute; background: white; border: 1px solid #ddd; border-radius: 10px; padding: 10px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); z-index: 1000; `; // 添加表情按钮 emojis.forEach(emoji => { const btn = document.createElement('button'); btn.textContent = emoji; btn.style.cssText = ` background: none; border: none; font-size: 1.5rem; cursor: pointer; padding: 5px; border-radius: 5px; `; btn.addEventListener('click', () => { messageInput.value += emoji; messageInput.focus(); updateCharCount(); document.body.removeChild(picker); }); btn.addEventListener('mouseenter', () => { btn.style.backgroundColor = '#f0f0f0'; }); btn.addEventListener('mouseleave', () => { btn.style.backgroundColor = 'transparent'; }); picker.appendChild(btn); }); // 定位并添加选择器 const rect = emojiBtn.getBoundingClientRect(); picker.style.top = `${rect.top - 150}px`; picker.style.left = `${rect.left}px`; document.body.appendChild(picker); // 点击外部关闭选择器 const closePicker = (e) => { if (!picker.contains(e.target) && e.target !== emojiBtn) { document.body.removeChild(picker); document.removeEventListener('click', closePicker); } }; setTimeout(() => { document.addEventListener('click', closePicker); }, 100); }); } // 事件监听器 joinBtn.addEventListener('click', joinChat); usernameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { joinChat(); } }); sendBtn.addEventListener('click', sendMessage); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); leaveBtn.addEventListener('click', leaveChat); messageInput.addEventListener('input', updateCharCount); // 房间切换事件 roomListItems.forEach(item => { item.addEventListener('click', () => { switchRoom(item.dataset.room); }); }); // 初始化表情选择器 initEmojiPicker(); // 初始字符计数 updateCharCount(); });
- 现在让我们扩展服务器功能,支持多房间聊天和消息历史记录。 // server/server.js - 扩展功能 // 在原有代码基础上添加以下内容 // 存储在线用户信息 const onlineUsers = new Map(); // 扩展Socket.IO连接处理 io.on('connection', (socket) => { console.log('新用户连接:', socket.id); // 用户加入聊天室 socket.on('join', async (username) => { socket.username = username; socket.room = 'general'; // 存储用户信息 onlineUsers.set(socket.id, { username: username, room: 'general', joinTime: new Date() }); socket.join('general'); console.log(`${username}加入了聊天室`); // 通知其他用户 socket.to('general').emit('user_joined', { username: username, time: new Date() }); // 发送欢迎消息 socket.emit('message', { username: '系统', text: `欢迎 ${username} 加入聊天室!`, time: new Date() }); // 发送当前在线用户列表 const usersInRoom = Array.from(onlineUsers.values()) .filter(user => user.room === 'general') .map(user => user.username); socket.emit('user_list', usersInRoom); // 发送最近的消息历史 try { const recentMessages = await Message.find({ room: 'general' }) .sort({ time: -1 }) .limit(50) .sort({ time: 1 }); recentMessages.forEach(msg => { socket.emit('message', { username: msg.username, text: msg.text, time: msg.time }); }); } catch (error) { console.error('获取消息历史失败:', error); } }); // 切换房间 socket.on('switch_room', async (data) => { const oldRoom = socket.room; const newRoom = data.newRoom; if (oldRoom === newRoom) return; // 离开旧房间 socket.leave(oldRoom); socket.to(oldRoom).emit('user_left', { username: socket.username, time: new Date() }); // 加入新房间 socket.join(newRoom); socket.room = newRoom; // 更新在线用户信息 if (onlineUsers.has(socket.id)) { onlineUsers.get(socket.id).room = newRoom; } // 通知新房间的用户 socket.to(newRoom).emit('user_joined', { username: socket.username, time: new Date() }); // 发送新房间的用户列表 const usersInNewRoom = Array.from(onlineUsers.values()) .filter(user => user.room === newRoom) .map(user => user.username); socket.emit('user_list', usersInNewRoom); // 发送新房间的消息历史 try { const recentMessages = await Message.find({ room: newRoom }) .sort({ time: -1 }) .limit(50) .sort({ time: 1 }); // 清空客户端当前消息 socket.emit('clear_messages'); // 发送历史消息 recentMessages.forEach(msg => { socket.emit('message', { username: msg.username, text: msg.text, time: msg.time }); }); } catch (error) { console.error('获取消息历史失败:', error); } }); // 处理私聊消息 socket.on('private_message', async (data) => { const { to, text } = data; // 查找目标用户的socket let targetSocketId = null; for (const [id, userInfo] of onlineUsers.entries()) { if (userInfo.username === to) { targetSocketId = id; break; } } if (targetSocketId) { // 发送私聊消息 io.to(targetSocketId).emit('private_message', { from: socket.username, text: text, time: new Date() }); // 保存私聊消息到数据库 const privateMessage = new Message({ username: socket.username, text: text, room: `private_${socket.username}_${to}`, time: new Date(), isPrivate: true, recipient: to }); try { await privateMessage.save(); } catch (error) { console.error('保存私聊消息失败:', error); } } else { // 用户不在线 socket.emit('error', { message: `用户 ${to} 不在线` }); } }); // 用户断开连接 socket.on('disconnect', () => { if (socket.username) { console.log(`${socket.username}断开连接`); // 通知房间内的其他用户 if (socket.room) { socket.to(socket.room).emit('user_left', { username: socket.username, time: new Date() }); } // 从在线用户列表中移除 onlineUsers.delete(socket.id); } }); }); // 添加API路由获取聊天统计信息 app.get('/api/stats', async (req, res) => { try { const totalMessages = await Message.countDocuments(); const totalUsers = await Message.distinct('username').count(); const messagesToday = await Message.countDocuments({ time: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) } }); res.json({ totalMessages, totalUsers, messagesToday, onlineUsers: onlineUsers.size }); } catch (error) { res.status(500).json({ error: '获取统计信息失败' }); } }); // 添加API路由获取房间列表 app.get('/api/rooms', async (req, res) => { try { const rooms = await Message.distinct('room', { isPrivate: { $ne: true } }); res.json(rooms); } catch (error) { res.status(500).json({ error: '获取房间列表失败' }); } });
- 让我们添加一些高级功能,如消息通知、文件上传和用户状态。 // public/js/client.js - 高级功能扩展 // 在原有代码基础上添加以下内容 // 检查浏览器通知权限 function checkNotificationPermission() { if ('Notification' in window) { if (Notification.permission === 'granted') { return true; } else if (Notification.permission !== 'denied') { Notification.requestPermission().then(permission => { return permission === 'granted'; }); } } return false; } // 显示通知 function showNotification(title, body) { if (checkNotificationPermission() && document.hidden) { const notification = new Notification(title, { body: body, icon: '/favicon.ico' }); notification.onclick = () => { window.focus(); notification.close(); }; setTimeout(() => notification.close(), 5000); } } // 文件上传功能 function initFileUpload() { const uploadBtn = document.getElementById('upload-btn'); const fileInput = document.createElement('input'); fileInput.type = 'file';
在当今数字化时代,实时在线聊天功能已成为网站和应用程序的标配功能。无论是电商平台的客服咨询、社交网站的即时通讯,还是企业内部协作工具,实时聊天功能都能显著提升用户体验和沟通效率。本教程将详细介绍如何为您的网站集成一个完整的实时在线聊天与通讯应用,包括前端界面、后端服务和数据库设计。
在开始开发之前,我们需要选择合适的技术栈:
- 前端:HTML5、CSS3、JavaScript(使用原生JS或框架如React/Vue)
- 后端:Node.js + Express.js
- 实时通信:Socket.IO(基于WebSocket的库)
- 数据库:MongoDB(存储用户信息和聊天记录)
- 部署:可以使用Heroku、AWS或Vercel等云服务
chat-application/
├── public/
│ ├── index.html
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── client.js
├── server/
│ ├── server.js
│ ├── models/
│ │ └── Message.js
│ └── routes/
│ └── api.js
├── package.json
└── README.md
chat-application/
├── public/
│ ├── index.html
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── client.js
├── server/
│ ├── server.js
│ ├── models/
│ │ └── Message.js
│ └── routes/
│ └── api.js
├── package.json
└── README.md
首先,我们需要创建一个基础的Express服务器,并集成Socket.IO用于实时通信。
// server/server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const cors = require('cors');
// 初始化Express应用
const app = express();
const server = http.createServer(app);
// 配置Socket.IO
const io = socketIo(server, {
cors: {
origin: "http://localhost:3000", // 前端地址
methods: ["GET", "POST"]
}
});
// 中间件配置
app.use(cors());
app.use(express.json());
app.use(express.static('public')); // 静态文件服务
// 连接MongoDB数据库
const mongoURI = 'mongodb://localhost:27017/chat_app';
mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB连接成功'))
.catch(err => console.log('MongoDB连接失败:', err));
// 定义消息模型
const Message = require('./models/Message');
// Socket.IO连接处理
io.on('connection', (socket) => {
console.log('新用户连接:', socket.id);
// 用户加入聊天室
socket.on('join', (username) => {
socket.username = username;
socket.join('general'); // 加入默认聊天室
console.log(`${username}加入了聊天室`);
// 通知其他用户
socket.to('general').emit('user_joined', {
username: username,
time: new Date()
});
// 发送欢迎消息
socket.emit('message', {
username: '系统',
text: `欢迎 ${username} 加入聊天室!`,
time: new Date()
});
});
// 处理聊天消息
socket.on('chat_message', async (data) => {
const message = new Message({
username: data.username,
text: data.text,
room: 'general',
time: new Date()
});
try {
// 保存消息到数据库
await message.save();
// 广播消息给所有用户
io.to('general').emit('message', {
username: data.username,
text: data.text,
time: new Date()
});
} catch (error) {
console.error('保存消息失败:', error);
}
});
// 用户断开连接
socket.on('disconnect', () => {
if (socket.username) {
console.log(`${socket.username}断开连接`);
socket.to('general').emit('user_left', {
username: socket.username,
time: new Date()
});
}
});
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
接下来,我们需要定义MongoDB数据模型来存储聊天消息。
// server/models/Message.js
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
username: {
type: String,
required: true,
trim: true
},
text: {
type: String,
required: true,
trim: true,
maxlength: 500
},
room: {
type: String,
default: 'general',
trim: true
},
time: {
type: Date,
default: Date.now
}
});
// 创建索引以提高查询效率
messageSchema.index({ room: 1, time: -1 });
module.exports = mongoose.model('Message', messageSchema);
现在让我们创建一个美观且功能完整的聊天界面。
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时在线聊天应用</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<!-- 登录界面 -->
<div id="login-container" class="login-container">
<div class="login-box">
<h1><i class="fas fa-comments"></i> 实时聊天室</h1>
<p>请输入用户名加入聊天</p>
<div class="input-group">
<input type="text" id="username" placeholder="请输入用户名" maxlength="20">
<button id="join-btn" class="btn-primary">
<i class="fas fa-sign-in-alt"></i> 加入聊天
</button>
</div>
<div class="room-selection">
<label for="room-select">选择聊天室:</label>
<select id="room-select">
<option value="general">综合聊天室</option>
<option value="tech">技术讨论</option>
<option value="games">游戏交流</option>
<option value="random">随机闲聊</option>
</select>
</div>
</div>
</div>
<!-- 聊天主界面 -->
<div id="chat-container" class="chat-container hidden">
<div class="chat-header">
<h2><i class="fas fa-comment-dots"></i> 实时聊天室</h2>
<div class="user-info">
<span id="current-user"></span>
<span id="current-room" class="room-badge"></span>
<button id="leave-btn" class="btn-secondary">
<i class="fas fa-sign-out-alt"></i> 退出
</button>
</div>
</div>
<div class="chat-main">
<!-- 在线用户列表 -->
<div class="sidebar">
<h3><i class="fas fa-users"></i> 在线用户 (<span id="user-count">0</span>)</h3>
<ul id="user-list"></ul>
<div class="room-list">
<h4><i class="fas fa-door-open"></i> 聊天室</h4>
<ul id="room-list">
<li class="active" data-room="general">综合聊天室</li>
<li data-room="tech">技术讨论</li>
<li data-room="games">游戏交流</li>
<li data-room="random">随机闲聊</li>
</ul>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="chat-area">
<div id="messages" class="messages">
<!-- 消息将通过JavaScript动态添加 -->
</div>
<!-- 消息输入区域 -->
<div class="message-input">
<div class="input-group">
<input type="text" id="message-input" placeholder="输入消息..." maxlength="500">
<button id="send-btn" class="btn-primary">
<i class="fas fa-paper-plane"></i> 发送
</button>
</div>
<div class="input-actions">
<button id="emoji-btn" class="btn-icon" title="表情">
<i class="far fa-smile"></i>
</button>
<button id="upload-btn" class="btn-icon" title="上传文件">
<i class="fas fa-paperclip"></i>
</button>
<span id="char-count">0/500</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Socket.IO客户端库 -->
<script src="/socket.io/socket.io.js"></script>
<!-- 自定义JavaScript -->
<script src="js/client.js"></script>
</body>
</html>
让我们为聊天应用添加现代化的样式。
/* public/css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 1200px;
height: 90vh;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
/* 登录界面样式 */
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
}
.login-box {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
text-align: center;
width: 100%;
max-width: 500px;
}
.login-box h1 {
color: #2575fc;
margin-bottom: 10px;
font-size: 2.5rem;
}
.login-box p {
color: #666;
margin-bottom: 30px;
font-size: 1.1rem;
}
.input-group {
display: flex;
margin-bottom: 20px;
}
.input-group input, .input-group select {
flex: 1;
padding: 15px;
border: 2px solid #e1e5eb;
border-radius: 10px 0 0 10px;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
.input-group input:focus, .input-group select:focus {
border-color: #2575fc;
}
.btn-primary {
background: linear-gradient(to right, #6a11cb, #2575fc);
color: white;
border: none;
padding: 15px 25px;
border-radius: 0 10px 10px 0;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.room-selection {
text-align: left;
margin-top: 20px;
}
.room-selection label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
.room-selection select {
width: 100%;
padding: 12px;
border-radius: 10px;
border: 2px solid #e1e5eb;
}
/* 聊天界面样式 */
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-header {
background: linear-gradient(to right, #6a11cb, #2575fc);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h2 {
font-size: 1.8rem;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.room-badge {
background: rgba(255, 255, 255, 0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9rem;
}
.btn-secondary {
background: transparent;
color: white;
border: 2px solid rgba(255, 255, 255, 0.5);
padding: 8px 15px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
}
.chat-main {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #e1e5eb;
padding: 20px;
overflow-y: auto;
}
.sidebar h3, .sidebar h4 {
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e1e5eb;
}
#user-list {
list-style: none;
margin-bottom: 30px;
}
#user-list li {
padding: 10px 15px;
margin-bottom: 8px;
background: white;
border-radius: 10px;
display: flex;
align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
#user-list li:before {
content: "•";
color: #4CAF50;
font-size: 2rem;
margin-right: 10px;
}
.room-list ul {
list-style: none;
}
.room-list li {
padding: 12px 15px;
margin-bottom: 8px;
background: white;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.room-list li:hover {
background: #eef5ff;
}
.room-list li.active {
background: #2575fc;
color: white;
font-weight: 600;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: white;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
}
.message {
margin-bottom: 20px;
padding: 15px;
border-radius: 15px;
max-width: 70%;
word-wrap: break-word;
}
.message.system {
background: #f0f0f0;
color: #666;
text-align: center;
max-width: 100%;
font-style: italic;
}
.message.received {
background: #f1f3f4;
align-self: flex-start;
border-bottom-left-radius: 5px;
}
.message.sent {
background: linear-gradient(to right, #6a11cb, #2575fc);
color: white;
align-self: flex-end;
border-bottom-right-radius: 5px;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.9rem;
}
.message-sender {
font-weight: 600;
}
.message-time {
opacity: 0.7;
}
.message-text {
line-height: 1.5;
}
.message-input {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.btn-icon {
background: none;
border: none;
color: #666;
font-size: 1.2rem;
cursor: pointer;
padding: 5px 10px;
border-radius: 5px;
transition: all 0.3s;
}
.btn-icon:hover {
background: #f0f0f0;
color: #2575fc;
}
#char-count {
color: #999;
font-size: 0.9rem;
}
.hidden {
display: none !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-main {
flex-direction: column;
}
.sidebar {
width: 100%;
height: 200px;
border-right: none;
border-bottom: 1px solid #e1e5eb;
}
.message {
max-width: 85%;
}
.chat-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
}
最后,我们需要编写客户端JavaScript代码来处理用户交互和Socket.IO通信。
// public/js/client.js
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
// public/js/client.js - 续接上文
const chatContainer = document.getElementById('chat-container');
const usernameInput = document.getElementById('username');
const joinBtn = document.getElementById('join-btn');
const leaveBtn = document.getElementById('leave-btn');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const messagesContainer = document.getElementById('messages');
const userList = document.getElementById('user-list');
const userCount = document.getElementById('user-count');
const currentUserSpan = document.getElementById('current-user');
const currentRoomSpan = document.getElementById('current-room');
const roomSelect = document.getElementById('room-select');
const roomListItems = document.querySelectorAll('#room-list li');
const charCount = document.getElementById('char-count');
const emojiBtn = document.getElementById('emoji-btn');
// 初始化变量
let socket;
let currentUser = '';
let currentRoom = 'general';
let onlineUsers = new Set();
// 连接Socket.IO服务器
function connectToServer() {
// 连接到后端服务器
socket = io('http://localhost:3000');
// 连接成功
socket.on('connect', () => {
console.log('已连接到服务器');
});
// 接收消息
socket.on('message', (data) => {
addMessage(data.username, data.text, data.time, data.username === currentUser ? 'sent' : 'received');
});
// 用户加入通知
socket.on('user_joined', (data) => {
addSystemMessage(`${data.username} 加入了聊天室`, data.time);
addUserToList(data.username);
});
// 用户离开通知
socket.on('user_left', (data) => {
addSystemMessage(`${data.username} 离开了聊天室`, data.time);
removeUserFromList(data.username);
});
// 错误处理
socket.on('connect_error', (error) => {
console.error('连接错误:', error);
alert('无法连接到服务器,请检查网络连接');
});
}
// 添加消息到聊天界面
function addMessage(username, text, time, type = 'received') {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const timeString = new Date(time).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-sender">${username}</span>
<span class="message-time">${timeString}</span>
</div>
<div class="message-text">${escapeHtml(text)}</div>
`;
messagesContainer.appendChild(messageDiv);
scrollToBottom();
}
// 添加系统消息
function addSystemMessage(text, time) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message system';
const timeString = new Date(time).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-text">
<i class="fas fa-info-circle"></i> ${escapeHtml(text)}
<span class="message-time">${timeString}</span>
</div>
`;
messagesContainer.appendChild(messageDiv);
scrollToBottom();
}
// 添加用户到在线列表
function addUserToList(username) {
if (onlineUsers.has(username)) return;
onlineUsers.add(username);
updateUserList();
}
// 从在线列表移除用户
function removeUserFromList(username) {
onlineUsers.delete(username);
updateUserList();
}
// 更新在线用户列表
function updateUserList() {
userList.innerHTML = '';
onlineUsers.forEach(user => {
const li = document.createElement('li');
li.textContent = user;
userList.appendChild(li);
});
userCount.textContent = onlineUsers.size;
}
// 滚动到消息底部
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// HTML转义防止XSS攻击
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 加入聊天室
function joinChat() {
const username = usernameInput.value.trim();
if (!username) {
alert('请输入用户名');
usernameInput.focus();
return;
}
if (username.length > 20) {
alert('用户名不能超过20个字符');
return;
}
currentUser = username;
currentRoom = roomSelect.value;
// 连接服务器
connectToServer();
// 发送加入请求
socket.emit('join', currentUser);
// 更新界面
currentUserSpan.textContent = currentUser;
currentRoomSpan.textContent = currentRoom;
// 切换到聊天界面
loginContainer.classList.add('hidden');
chatContainer.classList.remove('hidden');
// 添加自己到用户列表
addUserToList(currentUser);
// 聚焦到消息输入框
messageInput.focus();
}
// 发送消息
function sendMessage() {
const text = messageInput.value.trim();
if (!text) {
alert('请输入消息内容');
return;
}
if (text.length > 500) {
alert('消息不能超过500个字符');
return;
}
// 发送消息到服务器
socket.emit('chat_message', {
username: currentUser,
text: text,
room: currentRoom
});
// 清空输入框
messageInput.value = '';
updateCharCount();
}
// 离开聊天室
function leaveChat() {
if (socket) {
socket.disconnect();
}
// 重置状态
currentUser = '';
onlineUsers.clear();
messagesContainer.innerHTML = '';
userList.innerHTML = '';
usernameInput.value = '';
messageInput.value = '';
// 切换回登录界面
chatContainer.classList.add('hidden');
loginContainer.classList.remove('hidden');
}
// 切换聊天室
function switchRoom(roomName) {
if (roomName === currentRoom) return;
// 更新当前房间
currentRoom = roomName;
currentRoomSpan.textContent = roomName;
// 更新房间选择状态
roomListItems.forEach(item => {
if (item.dataset.room === roomName) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// 清空当前消息
messagesContainer.innerHTML = '';
// 发送房间切换请求到服务器
socket.emit('switch_room', {
username: currentUser,
oldRoom: currentRoom,
newRoom: roomName
});
// 添加系统消息
addSystemMessage(`您已切换到 ${roomName} 聊天室`, new Date());
}
// 更新字符计数
function updateCharCount() {
const length = messageInput.value.length;
charCount.textContent = `${length}/500`;
if (length > 450) {
charCount.style.color = '#ff6b6b';
} else if (length > 400) {
charCount.style.color = '#ffa726';
} else {
charCount.style.color = '#999';
}
}
// 初始化表情选择器
function initEmojiPicker() {
// 简单的表情列表
const emojis = ['😀', '😂', '😊', '😍', '👍', '👋', '🎉', '🔥', '❤️', '✨'];
emojiBtn.addEventListener('click', () => {
// 创建表情选择器
const picker = document.createElement('div');
picker.className = 'emoji-picker';
picker.style.cssText = `
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 10px;
padding: 10px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
z-index: 1000;
`;
// 添加表情按钮
emojis.forEach(emoji => {
const btn = document.createElement('button');
btn.textContent = emoji;
btn.style.cssText = `
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 5px;
border-radius: 5px;
`;
btn.addEventListener('click', () => {
messageInput.value += emoji;
messageInput.focus();
updateCharCount();
document.body.removeChild(picker);
});
btn.addEventListener('mouseenter', () => {
btn.style.backgroundColor = '#f0f0f0';
});
btn.addEventListener('mouseleave', () => {
btn.style.backgroundColor = 'transparent';
});
picker.appendChild(btn);
});
// 定位并添加选择器
const rect = emojiBtn.getBoundingClientRect();
picker.style.top = `${rect.top - 150}px`;
picker.style.left = `${rect.left}px`;
document.body.appendChild(picker);
// 点击外部关闭选择器
const closePicker = (e) => {
if (!picker.contains(e.target) && e.target !== emojiBtn) {
document.body.removeChild(picker);
document.removeEventListener('click', closePicker);
}
};
setTimeout(() => {
document.addEventListener('click', closePicker);
}, 100);
});
}
// 事件监听器
joinBtn.addEventListener('click', joinChat);
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
joinChat();
}
});
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
leaveBtn.addEventListener('click', leaveChat);
messageInput.addEventListener('input', updateCharCount);
// 房间切换事件
roomListItems.forEach(item => {
item.addEventListener('click', () => {
switchRoom(item.dataset.room);
});
});
// 初始化表情选择器
initEmojiPicker();
// 初始字符计数
updateCharCount();
});
现在让我们扩展服务器功能,支持多房间聊天和消息历史记录。
// server/server.js - 扩展功能
// 在原有代码基础上添加以下内容
// 存储在线用户信息
const onlineUsers = new Map();
// 扩展Socket.IO连接处理
io.on('connection', (socket) => {
console.log('新用户连接:', socket.id);
// 用户加入聊天室
socket.on('join', async (username) => {
socket.username = username;
socket.room = 'general';
// 存储用户信息
onlineUsers.set(socket.id, {
username: username,
room: 'general',
joinTime: new Date()
});
socket.join('general');
console.log(`${username}加入了聊天室`);
// 通知其他用户
socket.to('general').emit('user_joined', {
username: username,
time: new Date()
});
// 发送欢迎消息
socket.emit('message', {
username: '系统',
text: `欢迎 ${username} 加入聊天室!`,
time: new Date()
});
// 发送当前在线用户列表
const usersInRoom = Array.from(onlineUsers.values())
.filter(user => user.room === 'general')
.map(user => user.username);
socket.emit('user_list', usersInRoom);
// 发送最近的消息历史
try {
const recentMessages = await Message.find({ room: 'general' })
.sort({ time: -1 })
.limit(50)
.sort({ time: 1 });
recentMessages.forEach(msg => {
socket.emit('message', {
username: msg.username,
text: msg.text,
time: msg.time
});
});
} catch (error) {
console.error('获取消息历史失败:', error);
}
});
// 切换房间
socket.on('switch_room', async (data) => {
const oldRoom = socket.room;
const newRoom = data.newRoom;
if (oldRoom === newRoom) return;
// 离开旧房间
socket.leave(oldRoom);
socket.to(oldRoom).emit('user_left', {
username: socket.username,
time: new Date()
});
// 加入新房间
socket.join(newRoom);
socket.room = newRoom;
// 更新在线用户信息
if (onlineUsers.has(socket.id)) {
onlineUsers.get(socket.id).room = newRoom;
}
// 通知新房间的用户
socket.to(newRoom).emit('user_joined', {
username: socket.username,
time: new Date()
});
// 发送新房间的用户列表
const usersInNewRoom = Array.from(onlineUsers.values())
.filter(user => user.room === newRoom)
.map(user => user.username);
socket.emit('user_list', usersInNewRoom);
// 发送新房间的消息历史
try {
const recentMessages = await Message.find({ room: newRoom })
.sort({ time: -1 })
.limit(50)
.sort({ time: 1 });
// 清空客户端当前消息
socket.emit('clear_messages');
// 发送历史消息
recentMessages.forEach(msg => {
socket.emit('message', {
username: msg.username,
text: msg.text,
time: msg.time
});
});
} catch (error) {
console.error('获取消息历史失败:', error);
}
});
// 处理私聊消息
socket.on('private_message', async (data) => {
const { to, text } = data;
// 查找目标用户的socket
let targetSocketId = null;
for (const [id, userInfo] of onlineUsers.entries()) {
if (userInfo.username === to) {
targetSocketId = id;
break;
}
}
if (targetSocketId) {
// 发送私聊消息
io.to(targetSocketId).emit('private_message', {
from: socket.username,
text: text,
time: new Date()
});
// 保存私聊消息到数据库
const privateMessage = new Message({
username: socket.username,
text: text,
room: `private_${socket.username}_${to}`,
time: new Date(),
isPrivate: true,
recipient: to
});
try {
await privateMessage.save();
} catch (error) {
console.error('保存私聊消息失败:', error);
}
} else {
// 用户不在线
socket.emit('error', {
message: `用户 ${to} 不在线`
});
}
});
// 用户断开连接
socket.on('disconnect', () => {
if (socket.username) {
console.log(`${socket.username}断开连接`);
// 通知房间内的其他用户
if (socket.room) {
socket.to(socket.room).emit('user_left', {
username: socket.username,
time: new Date()
});
}
// 从在线用户列表中移除
onlineUsers.delete(socket.id);
}
});
});
// 添加API路由获取聊天统计信息
app.get('/api/stats', async (req, res) => {
try {
const totalMessages = await Message.countDocuments();
const totalUsers = await Message.distinct('username').count();
const messagesToday = await Message.countDocuments({
time: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) }
});
res.json({
totalMessages,
totalUsers,
messagesToday,
onlineUsers: onlineUsers.size
});
} catch (error) {
res.status(500).json({ error: '获取统计信息失败' });
}
});
// 添加API路由获取房间列表
app.get('/api/rooms', async (req, res) => {
try {
const rooms = await Message.distinct('room', { isPrivate: { $ne: true } });
res.json(rooms);
} catch (error) {
res.status(500).json({ error: '获取房间列表失败' });
}
});
让我们添加一些高级功能,如消息通知、文件上传和用户状态。
// public/js/client.js - 高级功能扩展
// 在原有代码基础上添加以下内容
// 检查浏览器通知权限
function checkNotificationPermission() {
if ('Notification' in window) {
if (Notification.permission === 'granted') {
return true;
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
return permission === 'granted';
});
}
}
return false;
}
// 显示通知
function showNotification(title, body) {
if (checkNotificationPermission() && document.hidden) {
const notification = new Notification(title, {
body: body,
icon: '/favicon.ico'
});
notification.onclick = () => {
window.focus();
notification.close();
};
setTimeout(() => notification.close(), 5000);
}
}
// 文件上传功能
function initFileUpload() {
const uploadBtn = document.getElementById('upload-btn');
const fileInput = document.createElement('input');
fileInput.type = 'file';


