如何使用JavaScript正确检测网站用户在线状态
问题根源
你现在遇到的异常由两个核心错误导致:
- 状态存储路径设计错误:你把用户全局在线状态和单聊会话关系绑定了,写入路径用了
status/${username}/${receiver}结构,只要页面里的全局变量receiver被其他逻辑(比如切换聊天窗口、输入框关联选中操作等)修改,状态就会被写到错误的用户节点下,直接覆盖其他用户的状态。另外这种结构完全无法处理同一用户多开标签页的场景,一个标签页触发离线就会把全局状态改错。 - 生命周期事件选型错误:
onload/onunload本身可靠性极差,浏览器崩溃、移动端后台杀进程、网络中断时onunload根本不会触发;如果你给输入框绑定的交互事件没有阻止冒泡,很容易误触发body上挂载的状态逻辑,导致输入时乱切状态。
可靠JavaScript实现方案(适配你正在用的Firebase Realtime Database)
不用额外加PHP服务端逻辑,纯前端结合Firebase自带能力就能实现准确的在线状态判断,核心逻辑如下:
1. 调整状态存储结构
把用户在线状态存在独立的用户节点下,不要和会话关系绑定,每个用户节点存3个字段:
state: 枚举值,online/away/offlinelastActive: 时间戳,记录用户最后一次操作的时间connectedCount: 数字,记录当前用户打开的活跃标签页数量,避免多标签时误判离线
2. 核心实现代码
// 替换你原来的changeStatus逻辑,页面加载完成后直接执行 function initUserStatus(currentUserId) { const userStatusRef = db.ref(`status/${currentUserId}`); const connectedRef = db.ref(".info/connected"); // 监听客户端和Firebase的连接状态 connectedRef.on("value", (snapshot) => { if (snapshot.val() === true) { // 客户端连接成功时,先给当前用户的在线标签数+1 const conCountRef = userStatusRef.child("connectedCount"); conCountRef.transaction((currentVal) => { return (currentVal || 0) + 1; }); // 注册断开连接时的自动操作:标签数-1,减到0就设为离线 const onDisconnectRef = conCountRef.onDisconnect(); onDisconnectRef.transaction((currentVal) => { if (currentVal === null) return 0; const newCount = currentVal - 1; if (newCount <= 0) { // 所有标签都断开,设置离线状态 userStatusRef.set({ state: "offline", lastActive: Date.now(), connectedCount: 0 }); return 0; } return newCount; }); // 连接成功先更新为在线状态 userStatusRef.update({ state: "online", lastActive: Date.now() }); } }); // 监听页面可见性变化,切后台/锁屏超过5分钟设为离开状态 let awayTimer = null; document.addEventListener("visibilitychange", () => { if (document.hidden) { awayTimer = setTimeout(() => { userStatusRef.update({ state: "away", lastActive: Date.now() }); }, 5 * 60 * 1000); } else { clearTimeout(awayTimer); userStatusRef.update({ state: "online", lastActive: Date.now() }); } }); // 监听用户活跃操作,防抖更新最后活跃时间,30秒内重复操作不重复写入 let activeDebounce = null; const activeEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"]; activeEvents.forEach(evt => { window.addEventListener(evt, () => { if (activeDebounce) return; activeDebounce = setTimeout(() => { userStatusRef.update({ state: "online", lastActive: Date.now() }); activeDebounce = null; }, 30 * 1000); }, { passive: true }); }); // 页面正常关闭时补一次状态更新,做逻辑兜底 window.addEventListener("beforeunload", () => { const conCountRef = db.ref(`status/${currentUserId}/connectedCount`); conCountRef.transaction((currentVal) => { if (!currentVal || currentVal -1 <=0) { userStatusRef.set({ state: "offline", lastActive: Date.now(), connectedCount: 0 }); return 0; } return currentVal -1; }); }); } // 页面加载完成后初始化,传入当前登录用户的唯一ID即可,不要传接收方ID // initUserStatus(当前登录用户的ID)
3. 关键注意点
- 不要在状态更新逻辑里混入接收方ID,用户在线状态是全局属性,和当前打开哪个聊天窗口无关
- 不要依赖全局变量存当前用户ID,初始化的时候直接作为参数传入,避免被其他逻辑篡改
- 多标签场景必须做引用计数,否则一个标签页关闭就会把用户全局状态设为离线
- Firebase自带的
onDisconnect是服务端触发的,哪怕浏览器崩溃、网络断开、设备断电,只要服务端检测到连接断开就会执行预设的写入操作,比前端自己监听unload事件可靠得多
内容的提问来源于stack exchange,提问作者smardzi420
相关产品推荐
相关产品推荐

