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

如何在Next.js中限制网站仅允许单标签页访问

Next.js环境下网页游戏单标签运行限制方案

针对Next.js环境,以下两种方案可实现网页游戏仅在单个标签页运行的需求,适配App Router和Page Router:

方案一:Broadcast Channel API 实时标签页通信(纯前端)

利用浏览器原生的Broadcast Channel API实现标签页间实时消息传递,新标签打开时立刻通知其他标签禁用游戏逻辑。

实现代码(客户端组件)

'use client';

import { useEffect } from 'react';

const SingleTabGuard = () => {
  useEffect(() => {
    const CHANNEL_NAME = 'game_single_tab_lock';
    const channel = new BroadcastChannel(CHANNEL_NAME);
    const currentTabId = crypto.randomUUID();

    // 新标签激活时,向所有标签发送激活通知
    channel.postMessage({ type: 'TAB_ACTIVATED', tabId: currentTabId });

    // 监听其他标签的激活消息
    const handleChannelMessage = (event) => {
      if (event.data.type === 'TAB_ACTIVATED' && event.data.tabId !== currentTabId) {
        alert('游戏已在其他标签页运行,请关闭后重试');
        // 全局标记当前标签已被锁定,游戏逻辑中需校验此状态
        window.gameLocked = true;
        // 可扩展:隐藏游戏界面、跳转至提示页等
      }
    };

    channel.addEventListener('message', handleChannelMessage);

    // 页面卸载时发送注销通知
    const handleBeforeUnload = () => {
      channel.postMessage({ type: 'TAB_DEACTIVATED', tabId: currentTabId });
      channel.close();
    };

    window.addEventListener('beforeunload', handleBeforeUnload);

    // 组件卸载时清理资源
    return () => {
      channel.removeEventListener('message', handleChannelMessage);
      window.removeEventListener('beforeunload', handleBeforeUnload);
      channel.postMessage({ type: 'TAB_DEACTIVATED', tabId: currentTabId });
      channel.close();
    };
  }, []);

  return null; // 无需渲染UI
};

export default SingleTabGuard;

使用方式

  • App Router:在游戏入口的page.jsx或layout.jsx顶部添加'use client',引入并渲染SingleTabGuard组件。
  • Page Router:直接在_app.js或游戏页面组件中引入并渲染该组件。

方案二:LocalStorage + 心跳校验(兼容旧浏览器+崩溃恢复)

通过LocalStorage记录活跃标签ID,结合后端心跳校验处理页面崩溃后的残留标识问题,兼容性更好。

前端组件代码(客户端)

'use client';

import { useEffect } from 'react';

const SingleTabGuard = () => {
  useEffect(() => {
    const LOCK_KEY = 'active_game_tab';
    const currentTabId = crypto.randomUUID();

    // 初始化检查现有活跃标签
    const existingTabId = localStorage.getItem(LOCK_KEY);
    if (existingTabId) {
      // 调用后端接口校验标签是否仍活跃
      fetch('/api/check-tab-status', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ tabId: existingTabId })
      })
      .then(res => res.json())
      .then(data => {
        if (data.isActive) {
          alert('游戏已在其他标签页运行,请关闭后重试');
          window.gameLocked = true;
        } else {
          // 旧标签已失效,更新为当前标签ID
          localStorage.setItem(LOCK_KEY, currentTabId);
          startHeartbeat(currentTabId);
        }
      });
    } else {
      localStorage.setItem(LOCK_KEY, currentTabId);
      startHeartbeat(currentTabId);
    }

    // 页面可见性变化时更新状态
    const handleVisibilityChange = () => {
      if (!document.hidden) {
        const activeTabId = localStorage.getItem(LOCK_KEY);
        if (activeTabId !== currentTabId) {
          alert('游戏已在其他标签页运行,请关闭后重试');
          window.gameLocked = true;
        }
      }
    };

    // 页面卸载时清理标识
    const handleBeforeUnload = () => {
      const activeTabId = localStorage.getItem(LOCK_KEY);
      if (activeTabId === currentTabId) {
        localStorage.removeItem(LOCK_KEY);
        stopHeartbeat();
      }
    };

    // 心跳函数:保持后端活跃状态记录
    let heartbeatInterval;
    const startHeartbeat = (tabId) => {
      heartbeatInterval = setInterval(() => {
        fetch('/api/keep-tab-active', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ tabId })
        });
      }, 5000); // 每5秒发送一次心跳
    };

    const stopHeartbeat = () => clearInterval(heartbeatInterval);

    document.addEventListener('visibilitychange', handleVisibilityChange);
    window.addEventListener('beforeunload', handleBeforeUnload);

    // 组件卸载清理
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
      window.removeEventListener('beforeunload', handleBeforeUnload);
      stopHeartbeat();
      const activeTabId = localStorage.getItem(LOCK_KEY);
      if (activeTabId === currentTabId) {
        localStorage.removeItem(LOCK_KEY);
      }
    };
  }, []);

  return null;
};

export default SingleTabGuard;

后端接口示例(Next.js API路由)

在app/api/check-tab-status/route.js(App Router)或pages/api/check-tab-status.js(Page Router)中实现:

// App Router示例:check-tab-status
export async function POST(request) {
  const { tabId } = await request.json();
  // 用内存/Redis存储活跃tab的最后心跳时间,生产环境建议用Redis
  const lastHeartbeat = global.activeTabs?.[tabId] || 0;
  const isActive = Date.now() - lastHeartbeat < 10000; // 10秒内有心跳则视为活跃

  return Response.json({ isActive });
}

// keep-tab-active接口
export async function POST(request) {
  const { tabId } = await request.json();
  if (!global.activeTabs) global.activeTabs = {};
  global.activeTabs[tabId] = Date.now();
  return Response.json({ success: true });
}

关键注意事项

  • 所有逻辑必须在客户端组件中实现,服务器组件无法访问浏览器API。
  • 游戏核心逻辑中需加入window.gameLocked状态校验,比如在初始化、用户操作触发点判断是否允许执行。
  • 方案一无需后端支持,实现简单;方案二适合需要处理页面崩溃场景的情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 08:45:28