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
相关产品推荐
相关产品推荐

