基于NodeJS、Express、Passport与Passport.SocketIo实现在线用户列表
嗨,你已经把Session存储和Passport-Socket.io的基础对接搞定了,这步已经很稳了!要实现带用户名的在线/离线用户列表,其实可以从「绑定连接与用户会话」「跟踪状态变化」这几个核心点入手,我给你拆解下具体方案:
1. 先把Socket连接和用户会话绑定起来
passport.socketio已经帮你把登录用户的信息挂载到socket.request.user上了,所以在用户建立Socket连接时,我们可以直接把用户和socket.id关联起来,用来跟踪在线状态:
示例代码(app.js中Socket.io部分)
// 开发环境可以用内存Map临时存,生产环境建议用MongoDB/Redis持久化 const onlineUsers = new Map(); io.on('connection', (socket) => { const currentUser = socket.request.user; // 只处理已登录用户 if (currentUser && currentUser._id) { const userId = currentUser._id.toString(); // 记录用户在线状态、用户名和socket连接ID onlineUsers.set(userId, { username: currentUser.username, socketId: socket.id, isOnline: true }); // 同步到MongoDB(可选,用来持久化状态) User.findByIdAndUpdate( userId, { isOnline: true, socketId: socket.id }, (err) => err && console.error('更新用户在线状态失败:', err) ); // 向所有客户端广播最新在线用户列表 io.emit('onlineUsersUpdated', Array.from(onlineUsers.values())); } // 处理用户断开连接的情况 socket.on('disconnect', () => { if (currentUser && currentUser._id) { const userId = currentUser._id.toString(); onlineUsers.delete(userId); // 更新MongoDB的离线状态 User.findByIdAndUpdate( userId, { isOnline: false, socketId: null }, (err) => err && console.error('更新用户离线状态失败:', err) ); // 再次广播更新后的列表 io.emit('onlineUsersUpdated', Array.from(onlineUsers.values())); } }); });
2. 监听Session变化(处理主动登出场景)
如果用户主动登出,会话会被销毁,这时候我们可以监听MongoStore的sess:destroy事件,主动更新用户的离线状态:
示例代码(app.js中SessionStore配置部分)
const sessionStore = new MongoStore({ mongooseConnection: mongoose.connection }); // 监听会话销毁事件 sessionStore.on('sess:destroy', (sessionId) => { // 从Mongo的sessions集合中找到对应会话,获取用户ID mongoose.connection.collection('sessions').findOne({ _id: sessionId }, (err, session) => { if (err || !session?.passport?.user) return; const userId = session.passport.user.toString(); onlineUsers.delete(userId); User.findByIdAndUpdate( userId, { isOnline: false }, (err) => err && console.error('登出时更新用户状态失败:', err) ); io.emit('onlineUsersUpdated', Array.from(onlineUsers.values())); }); });
3. 前端接收并展示用户列表
前端只需要监听后端广播的onlineUsersUpdated事件,就能实时更新在线/离线用户列表:
// 前端Socket代码示例 socket.on('onlineUsersUpdated', (users) => { const userListEl = document.getElementById('user-list'); // 渲染列表,用样式区分在线/离线状态 userListEl.innerHTML = users.map(user => ` <div class="user-item ${user.isOnline ? 'status-online' : 'status-offline'}"> ${user.username} </div> `).join(''); });
关于你提到的队列处理
如果之前尝试用队列来优化状态更新,其实可以把「用户状态同步到数据库」的操作放到队列中执行,避免阻塞主线程。比如用Bull队列封装:
示例代码(user.js中封装队列任务)
const Bull = require('bull'); const userStatusQueue = new Bull('user-status-update'); // 定义队列处理函数 userStatusQueue.process(async (job) => { const { userId, isOnline, socketId } = job.data; try { await User.findByIdAndUpdate(userId, { isOnline, socketId: isOnline ? socketId : null }); } catch (err) { console.error('队列更新用户状态失败:', err); } }); // 导出供app.js调用 module.exports = userStatusQueue;
然后在app.js的Socket连接/断开时,把任务加入队列:
const userStatusQueue = require('./user.js'); // 连接时加入队列 userStatusQueue.add({ userId: currentUser._id.toString(), isOnline: true, socketId: socket.id });
几个需要注意的细节
- 内存Map
onlineUsers在服务器重启后会清空,生产环境一定要用MongoDB或Redis持久化在线状态 - 如果支持多设备登录,要考虑同一个用户多个socket连接的情况(比如存多个socketId,或者覆盖旧连接)
- 确保你的Session中正确存储了用户ID(Passport默认会把用户ID存在
session.passport.user字段)
内容的提问来源于stack exchange,提问作者Gabriel
相关产品推荐
相关产品推荐

