问题背景
近期一个项目,一个官网管理系统,左侧有这些菜单。菜单通常是根据router路由来生成的。在以往的一些管理系统方案中,菜单的权限 通常是 给每个路由约定一个权限key,在用户登录后,调用后端的权限数据接口,比如 数据面板的key是1,全局配置的key是2。获取到的数据权限数组= [ 1 ],则表示 这个用户有数据面板的权限,没有2,则这个用户没有全局配置的权限。前端在是否渲染这个菜单和是否跳转到404(您无权限查看)就是判断要跳转的页面的key是否在后端返回的权限列表中。
let routes = [
{
path: "/",
element: <MainFrame />,
// element: <Navigate to="/home" />,
id: "300001",
tabs: false,
menu: true,
meta: {
title: "根路径",
},
// children: [
// {
// key: 'home',
// path: 'home',
// menu: false,
// element: <MainFrame />
// }
// ]
// children: [
// {
// key: "dathboard",
// path: "dathboard",
// menu: true,
// id: "0000001",
// icon: <PieChartOutlined />,
// element: withLoadingComponent(<WorkBench />)
// meta: {
// title: '数据面板',
// accessId: '',
// hideMenu: false,
// noAuth: true,
// },
// },
// {
// path: "globalConfig",
// key: 'globalConfig',
// id: '300000',
// icon: <SettingOutlined />,
// menu: true,
// meta: {
// title: "全局配置"
// },
// children: [
// {
// path: 'module',
// key: 'module',
// id: '300010',
// menu: true,
// icon: <UngroupOutlined />,
// meta: {
// title: "模型管理"
// }
// },
// {
// path: 'moduleFiled',
// key: 'moduleFiled',
// id: '300020',
// menu: true,
// icon: <DeploymentUnitOutlined />,
// meta: {
// title: "模型字段"
// }
// }
// ]
// },
// {
// path: "article",
// key: "article",
// id: "400000",
// icon: <FolderOpenOutlined />,
// menu: true,
// meta: {
// title: "内容管理"
// },
// children: [
// {
// path: "news",
// key: "news",
// id: "400010",
// menu: true,
// icon: <ReadOutlined />,
// element: withLoadingComponent(<NewsView />),
// meta: {
// title: "新闻中心"
// },
// // children: [
// // {
// // path: "new-add",
// // key: "new-add",
// // id: "400011",
// // element: withLoadingComponent(<AddNews />),
// // menu: true,
// // meta: {
// // title: "新闻发布",
// // accessId: '',
// // hideMenu: false,
// // noAuth: true
// // }
// // },
// // {
// // path: 'news-list',
// // key: "news-list",
// // id: "400012",
// // menu: true,
// // element: withLoadingComponent(<NewsList />),
// // meta: {
// // title: "新闻列表",
// // accessId: "",
// // hideMenu: false,
// // noAuth: true
// // }
// // }
// // ]
// },
// {
// path: "product",
// key: "product",
// id: "400020",
// menu: true,
// icon: <ReadOutlined />,
// element: withLoadingComponent(<ProductView />),
// meta: {
// title: "产品中心"
// },
// // children: [
// // {
// // path: "add",
// // key: "add",
// // id: "400021",
// // element: withLoadingComponent(<AddProduct />),
// // menu: true,
// // meta: {
// // title: "产品发布",
// // accessId: '',
// // hideMenu: false,
// // noAuth: true
// // }
// // },
// // {
// // path: 'list',
// // key: "list",
// // id: "400022",
// // menu: true,
// // element: withLoadingComponent(<ProductList />),
// // meta: {
// // title: "产品列表",
// // accessId: "",
// // hideMenu: false,
// // noAuth: true
// // }
// // }
// // ]
// },
// {
// path: "about",
// key: "about",
// id: "400030",
// icon: <ReadOutlined />,
// menu: true,
// element: withLoadingComponent(<AboutUs />),
// meta: {
// title: "关于我们"
// },
// },
// {
// path: "brand",
// key: "brand",
// id: "400040",
// menu: true,
// icon: <ReadOutlined />,
// element: withLoadingComponent(<BrandView />),
// meta: {
// title: "品牌文化"
// },
// },
// {
// path: "server",
// key: "server",
// id: "400050",
// menu: true,
// icon: <ReadOutlined />,
// element: withLoadingComponent(<ServiceView />),
// meta: {
// title: "客户服务"
// },
// },
// {
// path: 'pubinfo',
// key: 'pubinfo',
// id: "400060",
// menu: true,
// icon: <ReadOutlined />,
// element: withLoadingComponent(<PublicView />),
// meta: {
// title: "公开信息"
// },
// },
// ],
// },
// {
// path: "set",
// key: "set",
// id: "500000",
// menu: true,
// icon: <GlobalOutlined />,
// meta: {
// title: "网站管理"
// },
// children: [
// {
// path: "base-set",
// key: "base-set",
// id: "5000001",
// menu: true,
// icon: <HddOutlined />,
// element: withLoadingComponent(<BaseSetView />),
// meta: {
// title: "网站配置"
// },
// },
// {
// path: "setBanner",
// key: "setBanner",
// id: "500002",
// icon: <PictureOutlined />,
// element: withLoadingComponent(<BannerSetView />),
// menu: true,
// meta: {
// title: "轮播图片"
// },
// },
// {
// path: "cate-set",
// key: "cate-set",
// id: "500004",
// icon: <ProfileOutlined />,
// element: withLoadingComponent(<CateView />),
// menu: true,
// meta: {
// title: "栏目设置"
// },
// },
// {
// path: "float-advert",
// key: "float-advert",
// icon: <OneToOneOutlined />,
// element: withLoadingComponent(<FloatAdvertView />),
// id: "500005",
// menu: true,
// meta: {
// title: "侧边浮窗"
// },
// },
// {
// path: "advert",
// key: "advert",
// icon: <StarOutlined />,
// element: withLoadingComponent(<FloatAdvertView />),
// id: "500006",
// menu: true,
// meta: {
// title: "广告管理"
// },
// },
// {
// path: "security",
// key: "security",
// icon: <UnlockOutlined />,
// element: withLoadingComponent(<SafeConfig />),
// id: "500007",
// menu: true,
// meta: {
// title: "安全配置"
// },
// },
// {
// path: "access",
// key: "access",
// icon: <HistoryOutlined />,
// element: withLoadingComponent(<VisitorLogList />),
// id: "500008",
// menu: true,
// meta: {
// title: "访问统计"
// },
// },
// ],
// },
// {
// path: 'member',
// key: 'member',
// id: '600000',
// menu: false,
// icon: <WindowsOutlined />,
// meta: {
// title: '会员管理'
// }
// },
// {
// path: 'system',
// key: 'system',
// id: '700000',
// menu: true,
// icon: <SettingOutlined />,
// meta: {
// title: '系统管理'
// },
// children: [
// {
// path: 'permission',
// key: 'permission',
// id: '700010',
// menu: true,
// icon: <SecurityScanOutlined />,
// element: withLoadingComponent(<PerMissionView />),
// meta: {
// title: '角色权限'
// }
// },
// {
// path: 'operationLog',
// key: 'operationLog',
// id: '700020',
// menu: true,
// icon: <ScheduleOutlined />,
// element: withLoadingComponent(<SystemLogList />),
// meta: {
// title: '系统日志'
// }
// },
// {
// path: 'account',
// key: 'account',
// id: '700030',
// menu: true,
// icon: <UserOutlined />,
// element: withLoadingComponent(<SystemUserList />),
// meta: {
// title: '系统用户'
// }
// },
// ]
// }
// ]
},
{
path: "login",
menu: false,
element: withLoadingComponent(<Login />)
},
{
path: "*",
menu: false,
element: withLoadingComponent(<Page404 />)
}
]
但是涉及到内容管理系统,则存在一个需求可能 性,即 内容管理 下面这6个项,现在是只有6个项,而后期可能会有7个项,比如,通过如下 的栏目配置 新建一一个顶级栏目,
那么这个栏目和 公开信息那一栏平级,在内容管理那里 公开信息 下面就应该会有出现 刚刚添加的一级栏目。
所以,就决定把整路由部分由后端来进行返回,即前面示例代码中的那些。
后端返回的路由数据
决定把整个路由后端返回,所以,后端nodeJs返回JSON数据如下:
ctx.body = {
status: "0000",
message: "菜单权限获取成功",
data: {
list: [
{
key: "dathboard",
path: "dathboard",
menu: true,
id: "0000001",
icon: `PieChartOutlined`,
element: `WorkBench`,
meta: {
title: '数据面板',
// accessId: '',
// hideMenu: false,
// noAuth: true,
},
},
{
path: "globalConfig",
key: 'globalConfig',
id: '300000',
icon: `SettingOutlined`,
menu: true,
meta: {
title: "全局配置"
},
children: [
{
path: 'module',
key: 'module',
id: '300010',
menu: true,
icon: `UngroupOutlined`,
meta: {
title: "模型管理"
}
},
{
path: 'moduleFiled',
key: 'moduleFiled',
id: '300020',
menu: true,
icon: `DeploymentUnitOutlined`,
meta: {
title: "模型字段"
}
}
]
},
{
path: "article",
key: "article",
id: "400000",
icon: `FolderOpenOutlined`,
menu: true,
meta: {
title: "内容管理"
},
children: [
{
path: "news",
key: "news",
id: "400010",
menu: true,
icon: `ReadOutlined`,
element: 'NewsView',
meta: {
title: "新闻中心"
},
},
{
path: "product",
key: "product",
id: "400020",
menu: true,
icon: `ReadOutlined`,
element: `ProductView`,
meta: {
title: "产品中心"
},
},
{
path: "about",
key: "about",
id: "400030",
icon: `ReadOutlined`,
menu: true,
element: `AboutUs`,
meta: {
title: "关于我们"
},
},
{
path: "brand",
key: "brand",
id: "400040",
menu: true,
icon: `ReadOutlined`,
element: `BrandView`,
meta: {
title: "品牌文化"
},
},
{
path: "server",
key: "server",
id: "400050",
menu: true,
icon: `ReadOutlined`,
element: `ServiceView`,
meta: {
title: "客户服务"
},
},
{
path: 'pubinfo',
key: 'pubinfo',
id: "400060",
menu: true,
icon: `ReadOutlined`,
element: `ServiceView`,
meta: {
title: "公开信息"
},
},
],
},
{
path: "set",
key: "set",
id: "500000",
menu: true,
icon: `GlobalOutlined`,
meta: {
title: "网站管理"
},
children: [
{
path: "base-set",
key: "base-set",
id: "5000001",
menu: true,
icon: `HddOutlined`,
element: `BaseSetView`,
meta: {
title: "网站配置"
},
},
{
path: "setBanner",
key: "setBanner",
id: "500002",
icon: `PictureOutlined`,
element: `BannerSetView`,
menu: true,
meta: {
title: "轮播图片"
},
},
{
path: "cate-set",
key: "cate-set",
id: "500004",
icon: `ProfileOutlined`,
element: `CateView`,
menu: true,
meta: {
title: "栏目设置"
},
},
{
path: "float-advert",
key: "float-advert",
icon: `OneToOneOutlined`,
element: `FloatAdvertView`,
id: "500005",
menu: true,
meta: {
title: "侧边浮窗"
},
},
{
path: "advert",
key: "advert",
icon: `StarOutlined`,
element: `FloatAdvertView`,
id: "500006",
menu: true,
meta: {
title: "广告管理"
},
},
{
path: "security",
key: "security",
icon: `UnlockOutlined`,
element: `SafeConfig`,
id: "500007",
menu: true,
meta: {
title: "安全配置"
},
},
{
path: "access",
key: "access",
icon: `HistoryOutlined`,
element: `VisitorLogList`,
id: "500008",
menu: true,
meta: {
title: "访问统计"
},
},
],
},
{
path: 'member',
key: 'member',
id: '600000',
menu: false,
icon: `WindowsOutlined`,
meta: {
title: '会员管理'
}
},
{
path: 'system',
key: 'system',
id: '700000',
menu: true,
icon: `SettingOutlined`,
meta: {
title: '系统管理'
},
children: [
{
path: 'permission',
key: 'permission',
id: '700010',
menu: true,
icon: `SecurityScanOutlined`,
element: `PerMissionView`,
meta: {
title: '角色权限'
}
},
{
path: 'operationLog',
key: 'operationLog',
id: '700020',
menu: true,
icon: `ScheduleOutlined`,
element: `SystemLogList`,
meta: {
title: '系统日志'
}
},
{
path: 'account',
key: 'account',
id: '700030',
menu: true,
icon: `UserOutlined`,
element: `SystemUserList`,
meta: {
title: '系统用户'
}
},
]
}
]
}
}
后端返回权限路由,那么,就开发本地只保留*路由和login路由,因为这部分路由本地必须 要有。所以routes的如下:
let routes = [
{
path: "/",
element: <MainFrame />,
// element: <Navigate to="/home" />,
id: "300001",
tabs: false,
menu: true,
meta: {
title: "根路径",
},
children:[], //从后端请求返回来的数据就要放进这里面。
{
path: "login",
menu: false,
element: withLoadingComponent(<Login />)
},
{
path: "*",
menu: false,
element: withLoadingComponent(<Page404 />)
}
]
const setRoutes = (list) => {
routes = list
}
export {
setRoutes
}
export default routes;
即 从后端返回来的路由数据就放进routes[0].chilren 里面。当然,这个文件中默认导出routes,同时也导出setRoutes方法来改变这个routes数据。所以routes没有按平时的习惯写成const常量,得写成变量。
路由鉴权
路由鉴权这部分好说,主要是登录,判断用户是否登录,没有登录就给它跳转到登录页面,否则允许 它跳到需要的页面。在下面的示例中,如果跳转的是登录页面并且已经有token即已经登录,则返回到主页。如果不是跳到登录页面又没有token即没有登录,则跳到登录,重点是 return <Routers />
这个组件
路由鉴权组件:
const AuthRouter = () => {
const location = useLocation();
let token = getToken();
if (location.pathname === "/login" && token) {
return <ToHome />
}
if (location.pathname !== "/login" && !token) {
return <ToLogin />
}
return <Routers />
}
export default AuthRouter;
Routers组件:
const Route = ({ routes }) => {
let outer = useRoutes(routes)
return outer
}
const Routers = () => {
const [loading, setLoading] = useState(false)
const [curRoutes, setCurRoutes] = useState([...routes]);
// 动态获取路由
const getRoutes = () => {
http.post({
url: "/admin_users-api/getMenu",
before: () => setLoading(true),
params: {},
success: (res) => {
if (res.status === "0000") {
let list = routes;
list[0].children = getRouteList(res?.data?.list)
setCurRoutes(list)
setRoutes(list)
}
},
failure: () => { },
complete: () => setLoading(false)
})
}
useEffect(() => {
getRoutes()
}, [])
return loading ? <>
<LayoutLoad />
</> : <Route routes={curRoutes} />
}
export default Routers;
Routers组件中useEffect生命周期调用获取路由数据的函数。在拿到路由后,通过调用导入的setRoutes函数来改变routes路由数据数据,并且赋值给到组件本身的curRoutes状态。而curRoutes状态更新了,就会触发重新渲染 <Route routes={curRoutes} />
,从而实现动态路由。
方案研究中遇到的坑点
- 起初的时候return 那里的写法是下面这样的
return <Route routes={curRoutes} />
即直接返回Route,然后我就遇到404的问题,即 比如我跳转到http://localhost:3000/website-admin/article/about 路径的时候,如果按了浏览器的刷新按钮。会在有一瞬间浏览器会闪过显示404页面。分析了一下,原因是 直接刷新路由的时候,其实是因为还没有获得 /article/about 这个路由,即路由中还没有这个数据。因为还在请求数据当中。所以解决方案是 给一个loading状态,在loading的时候,让它显示加载中,于是就是使用了下面的代码。
return loading ? <>
<LayoutLoad />
</> : <Route routes={curRoutes} />
2.遇到的第二个坑是是在哪里获取权限的问题,在之前的项目中,我们获取用户权限是在MainFrame这个基础组件中获取权限,这两天发现,在上面的情况中,即直接在浏览器地址栏输入http://localhost:3000/website-admin/article/about 的时候,这个组件没有加载到,基础框架组件只有在访问 http://localhost:3000/ 这个根路由的时候才加载。所以昨天调试了好多次,发现获取路由权限就不能像之前那样写在MainFrame组件中。要写在路由鉴权组件这边。