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

如何使用JavaScript正确检测网站用户在线状态

问题根源

你现在遇到的异常由两个核心错误导致:

  • 状态存储路径设计错误:你把用户全局在线状态和单聊会话关系绑定了,写入路径用了status/${username}/${receiver}结构,只要页面里的全局变量receiver被其他逻辑(比如切换聊天窗口、输入框关联选中操作等)修改,状态就会被写到错误的用户节点下,直接覆盖其他用户的状态。另外这种结构完全无法处理同一用户多开标签页的场景,一个标签页触发离线就会把全局状态改错。
  • 生命周期事件选型错误:onload/onunload本身可靠性极差,浏览器崩溃、移动端后台杀进程、网络中断时onunload根本不会触发;如果你给输入框绑定的交互事件没有阻止冒泡,很容易误触发body上挂载的状态逻辑,导致输入时乱切状态。
可靠JavaScript实现方案(适配你正在用的Firebase Realtime Database)

不用额外加PHP服务端逻辑,纯前端结合Firebase自带能力就能实现准确的在线状态判断,核心逻辑如下:

1. 调整状态存储结构

把用户在线状态存在独立的用户节点下,不要和会话关系绑定,每个用户节点存3个字段:

  • state: 枚举值,online/away/offline
  • lastActive: 时间戳,记录用户最后一次操作的时间
  • 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 07:31:27