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

