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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 14:15:47