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

基于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 
});

几个需要注意的细节

  • 内存MaponlineUsers在服务器重启后会清空,生产环境一定要用MongoDB或Redis持久化在线状态
  • 如果支持多设备登录,要考虑同一个用户多个socket连接的情况(比如存多个socketId,或者覆盖旧连接)
  • 确保你的Session中正确存储了用户ID(Passport默认会把用户ID存在session.passport.user字段)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:31:22