大家好,我是前端小尧!今天我们把 UmiJS Max 的权限管理玩到极致:动态路由、无权限跳转、按钮级权限、模块化逻辑,还加了 Loading 组件 避免跳转闪烁,用 Zustand 管理角色状态告别刷新。代码案例丰富到爆,赶紧收藏起来吧!
一、目标:打造丝滑的权限系统
我们要实现:
- 动态路由:用 patchRoutes 根据角色生成菜单,支持后端返回。
- 无权限跳转:用 render 拦截,跳转到首个有权页面。
- 按钮级权限:用 Access 和 useAccess 精细化控制。
- 模块化:权限逻辑抽到 accessHelper.ts,复用性强。
- Loading 组件:跳转时显示加载动画,避免闪烁。
- Zustand 状态管理:动态切换角色,无需刷新。
二、项目准备:安装依赖
先安装 Zustand:
npm install zustand
启用 Umi 的 access 插件:
// .umirc.ts
export default {
access: {},
};
三、模块化权限逻辑:accessHelper.ts
权限逻辑抽到 src/utils/accessHelper.ts:
// src/utils/accessHelper.ts
export const defineAccess = (role: string | undefined) => {
return {
canAdmin: role === 'admin',
canUser: role === 'user',
canRead: role === 'admin' || role === 'user',
canDelete: role === 'admin', // 按钮级权限
};
};
// 路由权限映射表
export const routeAccessMap = {
'/home': ['canRead'],
'/admin': ['canAdmin'],
'/user': ['canUser'],
};
// 检查是否有权限访问路径
export const hasAccessToRoute = (path: string, access: any) => {
const requiredAccess = routeAccessMap[path] || [];
return requiredAccess.every((key) => access[key]);
};
// 找到首个有权限的页面
export const getFirstAccessibleRoute = (access: any) => {
return Object.keys(routeAccessMap).find((path) => hasAccessToRoute(path, access)) || '/home';
};
// 动态路由
export const getDynamicRoutes = (role: string) => {
const allRoutes = [
{ path: '/home', component: '@/pages/home', access: 'canRead' },
{ path: '/admin', component: '@/pages/admin', access: 'canAdmin' },
{ path: '/user', component: '@/pages/user', access: 'canUser' },
];
return allRoutes.filter((route) => {
if (!route.access) return true;
return role === 'admin' ? true : route.access !== 'canAdmin';
});
};
src/access.ts 引用:
// src/access.ts
import { defineAccess } from '@/utils/accessHelper';
export default function access(initialState: { role?: string } | undefined) {
return defineAccess(initialState?.role);
}
四、Zustand 管理角色状态
新建 src/store/roleStore.ts 用 Zustand 管理角色:
// src/store/roleStore.ts
import { create } from 'zustand';
import { getDynamicRoutes } from '@/utils/accessHelper';
interface RoleState {
role: string;
routes: any[];
setRole: (role: string) => void;
}
export const useRoleStore = create<RoleState>((set) => ({
role: 'user', // 默认角色
routes: getDynamicRoutes('user'), // 默认路由
setRole: (role) => set((state) => ({
role,
routes: getDynamicRoutes(role),
})),
}));
亮点:角色和路由状态统一管理,切换角色时无需刷新。
五、动态路由:用 patchRoutes 生成菜单
在 src/app.tsx 中实现动态路由和跳转逻辑:
// src/app.tsx
import { history, IRoute } from 'umi';
import { defineAccess, hasAccessToRoute, getFirstAccessibleRoute, getDynamicRoutes } from '@/utils/accessHelper';
import { useRoleStore } from '@/store/roleStore';
export async function getInitialState() {
return { role: 'user' }; // 模拟后端返回
}
// 动态路由
export const patchRoutes = ({ routes }: { routes: IRoute[] }) => {
const { role } = useRoleStore.getState();
const dynamicRoutes = getDynamicRoutes(role);
routes.length = 0;
routes.push(...dynamicRoutes);
};
// 无权限跳转 + Loading
let isRendering = false;
export const render = (oldRender: () => void) => {
const renderWithAccess = () => {
if (isRendering) return;
isRendering = true;
oldRender();
const { role } = useRoleStore.getState();
const access = defineAccess(role);
const currentPath = window.location.pathname;
if (!hasAccessToRoute(currentPath, access)) {
const redirectPath = getFirstAccessibleRoute(access);
history.replace(redirectPath);
}
isRendering = false;
};
renderWithAccess();
};
注意:isRendering 防止重复渲染,配合 Loading 使用。
六、Loading 组件:避免跳转闪烁
新建 src/components/Loading.tsx:
// src/components/Loading.tsx
import { Spin } from 'antd';
export default () => (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin tip="加载中..." size="large" />
</div>
);
在 src/app.tsx 中引入 Loading:
// src/app.tsx (续)
import { useState } from 'react';
import Loading from '@/components/Loading';
export const rootContainer = (container: React.ReactNode) => {
const [loading, setLoading] = useState(true);
setTimeout(() => setLoading(false), 500); // 模拟加载延迟
return loading ? <Loading /> : container;
};
效果:跳转时显示 Loading,避免页面闪烁。
七、按钮级权限:页面实现
在 /home 页面实现角色切换和按钮控制:
// src/pages/home.tsx
import { useAccess, Access } from 'umi';
import { useRoleStore } from '@/store/roleStore';
import { history } from 'umi';
export default () => {
const access = useAccess();
const { role, setRole } = useRoleStore();
const switchRole = (newRole: string) => {
setRole(newRole);
const newAccess = defineAccess(newRole);
const currentPath = window.location.pathname;
if (!hasAccessToRoute(currentPath, newAccess)) {
const redirectPath = getFirstAccessibleRoute(newAccess);
history.push(redirectPath); // 使用 push 代替刷新
}
};
return (
<div>
<h1>欢迎来到首页 (当前角色: {role})</h1>
<button onClick={() => switchRole('admin')}>切换为管理员</button>
<button onClick={() => switchRole('user')}>切换为普通用户</button>
<button>查看详情</button>
<Access accessible={access.canDelete} fallback={<span>无删除权限</span>}>
<button onClick={() => alert('删除成功')}>删除</button>
</Access>
{access.canAdmin && <button>管理员专用按钮</button>}
</div>
);
};
其他页面:
// src/pages/admin.tsx
export default () => <div>管理员专属页面</div>;
// src/pages/user.tsx
export default () => <div>普通用户专属页面</div>;
效果:
- 点击切换角色,路由和权限实时更新,无需刷新。
- role: 'user' 无删除权限,访问 /admin 跳到 /user。
- Loading 在跳转时显示,体验丝滑。
八、总结:六大功能完美实现
- 动态路由:patchRoutes 根据角色生成菜单。
- 无权限跳转:render 拦截,跳转首个有权页面。
- 按钮级权限:Access 和 useAccess 精细控制。
- 模块化:accessHelper.ts 抽离逻辑。
- Loading 组件:rootContainer 加加载动画。
- Zustand:角色状态管理,无刷新切换。
小贴士:
- Loading 时间可根据接口响应调整。
- Zustand 还能加持久化(persist 中间件)。
今天的分享干货满满吧!代码直接跑起来就行,觉得有用记得点赞收藏,有问题留言讨论哦~下次聊聊 Umi 的更多玩法,敬请期待!