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

NextJS中基于localStorage处理ACL菜单权限的最优实现方式

优化无权限菜单控制的LocalStorage使用方案

一、干掉不合理的轮询:用主动触发替代被动轮询

轮询本质是在等登录流程把数据写入LocalStorage,但完全可以让登录流程主动通知组件更新状态,配合初始读取+多标签页监听,彻底替代轮询:

  • 初始加载直接读取:组件挂载时立刻读一次LocalStorage,没数据就先维持空状态,不用等轮询
  • 登录成功主动同步状态:在登录接口调用成功后,拿到ACL数据时,不仅写入LocalStorage,同时直接调用setUserAcl更新组件状态,不用依赖LocalStorage的间接读取
  • 多标签页场景补充监听:如果需要支持多标签页登录同步,监听window的storage事件,当userInfo变化时自动更新状态

二、解决TypeScript的any类型报错:用类型守卫做安全解析

直接断言JSON.parse(userInfo)为UserInfo风险高,写个类型守卫函数,确保解析后的数据符合预期,彻底避免类型报错:

// 定义你的UserInfo类型(根据实际情况补充字段)
interface UserInfo {
  acl?: string[];
  aclCustom?: string[];
}

// 类型守卫:判断数据是否为合法的UserInfo
function isUserInfo(data: unknown): data is UserInfo {
  return (
    typeof data === 'object' &&
    data !== null &&
    (Array.isArray((data as UserInfo).acl) || Array.isArray((data as UserInfo).aclCustom))
  );
}

三、优化后的完整代码示例

菜单组件内的状态处理逻辑

import { useEffect, useState } from 'react';

interface UserInfo {
  acl?: string[];
  aclCustom?: string[];
}

function isUserInfo(data: unknown): data is UserInfo {
  return (
    typeof data === 'object' &&
    data !== null &&
    (Array.isArray((data as UserInfo).acl) || Array.isArray((data as UserInfo).aclCustom))
  );
}

function MenuComponent() {
  const [userAcl, setUserAcl] = useState<string[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  // 封装LocalStorage读取逻辑
  const loadUserAclFromStorage = () => {
    try {
      const userInfoStr = localStorage.getItem('userInfo');
      if (!userInfoStr) {
        setUserAcl([]);
        setIsLoading(false);
        return;
      }
      const userInfo = JSON.parse(userInfoStr);
      if (isUserInfo(userInfo)) {
        setUserAcl(userInfo.acl || userInfo.aclCustom || []);
      } else {
        console.error('LocalStorage中userInfo格式不合法');
        setUserAcl([]);
      }
    } catch (e) {
      console.error('解析userInfo失败', e);
      setUserAcl([]);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    // 初始加载LocalStorage数据
    loadUserAclFromStorage();

    // 监听多标签页的storage变化
    const handleStorageChange = (e: StorageEvent) => {
      if (e.key === 'userInfo') {
        loadUserAclFromStorage();
      }
    };
    window.addEventListener('storage', handleStorageChange);

    // 监听自定义事件:登录成功后触发更新
    const handleLoginSuccess = () => {
      loadUserAclFromStorage();
    };
    window.addEventListener('loginSuccess', handleLoginSuccess as EventListener);

    // 清理监听
    return () => {
      window.removeEventListener('storage', handleStorageChange);
      window.removeEventListener('loginSuccess', handleLoginSuccess as EventListener);
    };
  }, []);

  // 加载中状态:避免初始加载直接显示404
  if (isLoading) {
    return <div>加载中...</div>;
  }

  // 无权限时显示404
  if (userAcl.length === 0) {
    return <div>404 无访问权限</div>;
  }

  // 渲染有权限的菜单
  return (
    <ul>
      {userAcl.map((menuKey) => (
        <li key={menuKey}>{menuKey}</li>
      ))}
    </ul>
  );
}

登录流程中的触发逻辑

在登录接口成功后,写入LocalStorage并触发自定义事件:

async function login(username: string, password: string) {
  const res = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ username, password }),
  });
  const userInfo = await res.json();

  // 写入LocalStorage
  localStorage.setItem('userInfo', JSON.stringify(userInfo));
  
  // 触发自定义事件,通知菜单组件更新状态
  window.dispatchEvent(new CustomEvent('loginSuccess'));
}

方案优势

  • 完全移除轮询,避免不必要的性能消耗,状态更新更及时
  • 类型守卫确保TypeScript类型安全,彻底消除any类型报错
  • 支持多标签页登录状态同步
  • 增加加载中状态,避免初始加载时直接跳404的突兀体验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:32:04