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

React+Socket.io聊天应用:仅窗口关闭时发送removeUser请求而非刷新

区分浏览器关闭与页面刷新,精准触发用户移除操作

我完全懂你的需求——想在浏览器窗口关闭时移除后端的用户记录并清除会话存储,但页面刷新时要保留用户状态,之前用window.performance.navigation.type不管用对吧?这个API其实已经被废弃了,而且就算用新的API,也很难直接在beforeunload事件里区分关闭和刷新,因为两种操作都会触发这个事件。下面给你两个可行的方案:

方案一:前端定时器+页面加载校验

核心思路是:页面刷新时,浏览器会在触发beforeunload后马上重新加载页面,我们可以利用这个特性,在beforeunload里设置一个延迟执行的移除操作,然后在页面重新加载完成时取消这个操作;而窗口关闭时不会有后续的加载,定时器就会正常执行。

代码实现

首先在组件里添加定时器变量:

constructor(props) {
  super(props);
  this.unloadTimer = null; // 存储定时器ID
}

然后修改你的卸载事件逻辑:

removeUserBeforeUnload = () => {
  // 设置200ms延迟,给刷新页面的重新加载留时间
  this.unloadTimer = setTimeout(() => {
    const currentUser = this.props.currentUser;
    if (currentUser) {
      // 发送移除用户的socket请求
      socket.emit("removeUser", currentUser);
      // 清除redux-persist的会话存储(根据你的配置调整key)
      sessionStorage.removeItem('persist:root');
    }
  }, 200);
};

setupBeforeUnloadListener = () => {
  window.addEventListener("beforeunload", (ev) => {
    ev.preventDefault();
    this.removeUserBeforeUnload();
    // 部分浏览器需要返回值才能触发默认的确认提示(可选,不需要可以删掉)
    ev.returnValue = '';
  });
};

最后在页面加载完成时取消定时器(仅在刷新时生效):

componentDidMount() {
  this.setupBeforeUnloadListener();
  
  // 用新的导航类型API判断是否是页面刷新
  const navEntry = performance.getEntriesByType('navigation')[0];
  if (navEntry?.type === 'reload') {
    clearTimeout(this.unloadTimer);
  }

  // 刷新时重新同步用户状态到后端(可选,确保在线状态正确)
  if (this.props.currentUser) {
    socket.emit("userOnline", this.props.currentUser);
  }
}

方案二:后端超时校验+前端重连通知

这个方案更可靠,因为前端的事件判断可能受浏览器行为影响,后端通过监听socket断开后的重连情况来决定是否移除用户。

后端代码(Node.js/Express/Socket.io)

// 存储用户对应的socket和超时定时器
const userOnlineMap = new Map();

io.on('connection', (socket) => {
  // 用户连接/重连时触发
  socket.on('userReconnect', (username) => {
    // 如果之前有未执行的移除定时器,清除它
    if (userOnlineMap.has(username)) {
      clearTimeout(userOnlineMap.get(username).timeout);
    }
    // 更新用户的socket信息
    userOnlineMap.set(username, {
      socketId: socket.id,
      timeout: null
    });
  });

  // socket断开时触发
  socket.on('disconnect', () => {
    // 找到当前socket对应的用户
    let targetUser = null;
    for (const [username, data] of userOnlineMap.entries()) {
      if (data.socketId === socket.id) {
        targetUser = username;
        break;
      }
    }

    if (targetUser) {
      // 设置5秒超时,如果5秒内没有重连,就移除用户
      const timeout = setTimeout(() => {
        userOnlineMap.delete(targetUser);
        // 这里可以添加更新在线用户列表等逻辑
        io.emit('userOffline', targetUser);
      }, 5000);
      userOnlineMap.get(targetUser).timeout = timeout;
    }
  });
});

前端代码配合

在页面加载完成时(包括刷新)发送重连通知:

componentDidMount() {
  this.setupBeforeUnloadListener();
  
  // 页面加载时通知后端用户重连
  if (this.props.currentUser) {
    socket.emit("userReconnect", this.props.currentUser);
  }
}

// 窗口关闭时的处理(可选,配合后端超时更稳妥)
removeUserBeforeUnload = () => {
  const currentUser = this.props.currentUser;
  if (currentUser) {
    // 可以发送一个即时的移除请求,后端收到后直接移除
    socket.emit("removeUser", currentUser);
    sessionStorage.removeItem('persist:root');
  }
};

为什么之前的方法没用?

window.performance.navigation.type已经被MDN标记为废弃API,现在推荐使用performance.getEntriesByType('navigation')[0].type来获取导航类型,但就算用这个,也无法在beforeunload事件里区分窗口关闭和页面刷新——因为两种操作触发这个事件时,导航类型都是reload或者navigate,浏览器不会给你明确的区分标识,所以必须结合生命周期或后端逻辑来判断。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:27:31