NextJS网页聊天应用:关闭标签页踢用户、刷新不触发及移动端适配
实现方案
1. 核心思路:区分「关闭标签页」和「刷新/切换APP」
- 刷新操作:页面卸载后会立即重新加载,后端通过短时间延迟校验判断用户是否会重新连接
- 关闭标签页/浏览器:页面卸载后无后续连接,后端在延迟时间内未收到新连接则执行踢人逻辑
- 移动端切换APP:仅触发页面隐藏(
visibilitychange),此时不执行踢人,仅在页面隐藏后长时间未恢复或确实触发卸载时处理
2. 前端实现(NextJS 适配)
2.1 监听页面卸载与可见性变化
在聊天页面组件中用useEffect添加监听逻辑:
import { useEffect, useState } from 'react'; const ChatPage = () => { const [isPageVisible, setIsPageVisible] = useState(true); const userId = '当前用户ID'; // 替换为实际业务的用户ID useEffect(() => { // 处理页面可见性变化(适配移动端切换APP/锁屏) const handleVisibilityChange = () => { const currentVisible = document.visibilityState === 'visible'; setIsPageVisible(currentVisible); fetch(`/api/user/${currentVisible ? 'active' : 'hide'}`, { method: 'POST', body: JSON.stringify({ userId }), headers: { 'Content-Type': 'application/json' } }); }; // 处理页面卸载事件 const handleBeforeUnload = (e) => { e.preventDefault(); // 用keepalive确保页面卸载时请求能发送完成 fetch('/api/user/unload', { method: 'POST', body: JSON.stringify({ userId }), headers: { 'Content-Type': 'application/json' }, keepalive: true }); }; document.addEventListener('visibilitychange', handleVisibilityChange); window.addEventListener('beforeunload', handleBeforeUnload); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [userId]); return ( // 聊天页面内容 ); }; export default ChatPage;
2.2 关键细节
keepalive: true:避免页面卸载时浏览器中断请求,确保后端能收到卸载通知- 不要在
beforeunload中阻止默认行为(比如弹出确认框),仅发送状态通知即可
3. 后端逻辑配合
3.1 状态存储与校验
用Redis或内存维护用户离线标记,设置短延迟判断是否真的离线:
// Node.js + Redis 示例 const redis = require('redis'); const client = redis.createClient(); // 接收用户卸载通知 app.post('/api/user/unload', async (req, res) => { const { userId } = req.body; // 存储卸载标记,3秒后过期 await client.setEx(`user:unload:${userId}`, 3, Date.now()); res.status(200).send('ok'); }); // 接收用户激活/重连通知 app.post('/api/user/active', async (req, res) => { const { userId } = req.body; // 删除卸载标记,说明用户仍在线 await client.del(`user:unload:${userId}`); // 更新在线状态(5分钟过期,配合心跳兜底) await client.setEx(`user:online:${userId}`, 300, 'online'); res.status(200).send('ok'); }); // 定时检查离线标记 setInterval(async () => { const keys = await client.keys('user:unload:*'); for (const key of keys) { const userId = key.replace('user:unload:', ''); const unloadTime = await client.get(key); // 超过3秒未收到重连请求,执行踢人逻辑 if (Date.now() - unloadTime > 3000) { kickUserFromChat(userId); // 自定义踢人逻辑:移除房间、通知其他成员 await client.del(key); await client.del(`user:online:${userId}`); } } }, 1000);
3.2 心跳兜底
前端每30秒发送一次心跳请求,防止浏览器崩溃、移动端后台被回收等极端场景下的状态误判:
// 在ChatPage组件的useEffect中添加 useEffect(() => { const heartbeat = setInterval(() => { if (isPageVisible) { fetch('/api/user/heartbeat', { method: 'POST', body: JSON.stringify({ userId }), headers: { 'Content-Type': 'application/json' } }); } }, 30000); return () => clearInterval(heartbeat); }, [userId, isPageVisible]);
4. 移动端适配要点
- 优先用
visibilitychange事件:移动端切换APP、锁屏都会触发该事件,比pagehide/pageshow更稳定 - 避免误踢:仅当页面隐藏超过30秒且无心跳,或触发
beforeunload时才执行踢人 - 处理系统回收:移动端APP后台被系统强制回收时,心跳会中断,后端通过心跳过期自动踢人
内容的提问来源于stack exchange,提问作者Oscar
相关产品推荐
相关产品推荐

