找回密码
 会员注册
查看: 13|回复: 0

统一路由、菜单、面包屑和权限配置

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
73021
发表于 2024-10-2 12:51:26 | 显示全部楼层 |阅读模式
我最近做的一个新项目是一个典型的中后台项目,采用的是 React + React Router + Antd 方案。正常情况下我们需要定义路由配置,在页面中定义面包屑的数据,页面写完之后需要在左侧菜单中增加页面的路由。写多了之后,我会觉得同一个路由的相关信息在不同的地方重复声明,实在是有点麻烦,为什么我们不统一在一个地方定义,然后各个使用的地方动态获取呢?单独配置首先我们看看每个功能单独定义是如何配置的,之后我们再总结规律整理成一份通用的配置。路由和权限路由我们使用了react-router-config[1]进行了声明化的配置。//router.tsimport{RouteConfig}from'react-router-config';importDefaultLayoutfrom'./layouts/default';importGoodsListfrom'./pages/goods-list';importGoodsItemfrom'./pages/goods-item';exportconstroutes:RouteConfig[]=[{componentefaultLayout,routes:[{path:'/goods',exact:true,title:'商品列表',component:GoodsList,},{path:'/goods/:id',exact:true,title:'商品详情',component:GoodsItem,}],},];//app.tsximportReactfrom'react';import{BrowserRouterasRouter}from'react-router-dom';import{renderRoutes}from'react-router-config';import{routes}from'./router';exportdefaultfunctionApp(){return{renderRoutes(routes)};};菜单左侧导航菜单我们使用的是[2]组件,大概的方式如下://./layouts/defaultimportReactfrom'react';import{renderRoutes}from'react-router-config';import{Layout,Menu}from'antd';exportdefaultfunction({route}){return(Header商品列表{renderRoutes(route.routes)});}权限这里的权限主要指的是页面的权限。我们会请求一个服务端的权限列表接口,每个页面和功能都对应一个权限点,后台配置后告知我们该用户对应的权限列表。所以我们只需要记录每个页面对应的权限点,并在进入页面的时候判断下对应的权限点在不在返回的权限列表数据中即可。而页面权限与页面是如此相关,所以我们惯性的会将页面的权限点与页面路由配置在一块,再在页面统一的父组件中进行权限点的判断。//router.tsimport{RouteConfig}from'react-router-config';importDefaultLayoutfrom'./layouts/default';importGoodsListfrom'./pages/goods-list';importGoodsItemfrom'./pages/goods-item';exportconstroutes:RouteConfig[]=[{componentefaultLayout,routes:[{path:'/goods',exact:true,title:'商品列表',component:GoodsList,permission:'goods',},{path:'/goods/:id',exact:true,title:'商品详情',component:GoodsItem,permission:'goods-item',}],},];//./layouts/defaultimportReact,{useEffect,useMemo}from'react';import{useHistory,useLocation}from'react-router-dom';import{matchRoutes}from'react-router-config';exportdefaultfunction({route}){consthistory=useHistory();constlocation=useLocation();constpage=useMemo(()=>matchRoutes(route.routes,location.pathname).[0].route,[location.pathname,route.routes,]);useEffect(()=>{getPermissionList().then(permissions=>{if(page.permission&!permissions.includes(page.permission)){history.push('/no-permission');}})},[]);}面包屑面包屑则比较简单了,直接使用 即可//./pages/goods-item.tsximportReactfrom'react';import{Link}from'react-router-dom';import{Breadcrumb}from'antd';exportdefaultfunction(){return( 商品列表 商品详情);}合并配置通过上面的整理我们可以看到,所有的功能都是和配置相关,所有的配置都是对应路由的映射。虽然路由本身是平级的,但由于菜单和面包屑属于多级路由关系,所有我们的最终配置最好是多级嵌套,这样可以记录层级关系,生成菜单和面包屑比较方便。最终我们定义的配置结构如下://router-config.tsimporttype{RouterConfig}from'react-router-config';importGoodsListfrom'./pages/goods-list';importGoodsItemfrom'./pages/goods-item';exportinterfacePathConfigextendsRouterConfig{menu:boolean;permission:string;childrenathConfig[];}exportconstrouters=[{path:'/goods',exact:true,title:'商品列表',component:GoodsList,children:[{path:'/goods/:id',exact:true,title:'商品详情',component:GoodsItem}]}];路由基于上面的嵌套配置,我们需要定义一个flatRouters()方法将其进行打平,替换原来的配置即可。//router.tsimport{RouteConfig}from'react-router-config';importDefaultLayoutfrom'./layouts/default';import{routers,PathConfig}from'./router-config';functionflatRouters(routersathConfig[])athConfig[]{constresults=[];for(leti=0;i=()=>({routers.filter(({menu})=>menu).map(({title,path,children})=>(Array.isArray(children)&children.filter(({menu})=>menu).length({children.filter(({menu})=>menu).map(({title,path})=>())}))))});constNavMenuItem:React.FC=({path,title})=>({/^https:///.test(path)({title}){title})});exportdefaultfunction({route}){return(Header{renderRoutes(route.routes)});};面包屑面包屑的难点在于我们需要根据当前页面路由,不仅找到当前路由,还需要找到它的各种父级路由。除了定义一个findCrumb()方法来查找路由之外,为了方便查找,还在配置上做了一些约定。如果两个路由是父子关系,那么他们的路由路径也需要是包含关系。例如商品列表和商品详情是父子路由关系,商品列表的路径是/goods,那么商品详情的路由则应该为/goods/:id。这样在递进匹配查找的过程中,只需要判断当前页面路由是否包含该路径即可,减小了查找的难度。另外还有一个问题大家可能会注意到,商品详情的路由路径是/goods/:id,由于带有命名参数,当前路由去做字符串匹配的话肯定是没办法匹配到的。所以需要对命名参数进行正则通配符化,方便做路径的匹配。命名参数除了影响路径查找之外,还会影响面包屑的链接生成。由于带有命名参数,我们不能在面包屑中直接使用该路径作为跳转路由。为此我们还需要写一个stringify()方法,通过当前路由获取到所有的参数列表,并对路径中的命名参数进行替换。这也是为什么之前我们需要将父子路由的路径定义成包含关系。子路由在该条件下肯定会包含父级路径中所需要的参数,极大的方便我们父级路由的生成。//src/components/breadcrumb.tsximportReact,{useMemo}from'react';import{BreadcrumbasOBreadcrumb,BreadcrumbProps}from'antd';import{useHistory,useLocation,useParams}from'react-router';importRouters,{PathConfig}from'../router-config';functionfindCrumb(routersathConfig[],pathname:string)athConfig[]{constretathConfig[]=[];constrouter=routers.filter(({path})=>path!=='/').find(({path})=>newRegExp(`^${path.replace(/:[a-zA-Z]+/g,'.+').replace(///g,'\/')}`,'i').test(pathname));if(!router){returnret;}ret.push(router);if(Array.isArray(router.children)){ret.push(...findCrumb(router.children,pathname));}returnret;}functionstringify(path:string,params:Record){returnpath.replace(/[a-zA-Z]+)/g,(placeholder,key)=>params[key]||placeholder);}constBreadcrumb=React.memo (props=>{consthistory=useHistory();constparams=useParams();constlocation=useLocation();constroutersathConfig[]=useMemo (()=>findCrumb(Routers,location.pathname).slice(1),[location.pathname]);if(!routers.length||routers.length({name,onClick:idx!==routers.length-1()=>history.push(stringify(path,params)):undefined,}));return({data.map(({name,onClick})=>( {name}))});});exportdefaultBreadcrumb;后记至此我们的统一配置基本上就屡清楚了,我们发现只是简单的增加了几个属性,就让所有的配置统一到了一起。甚至我们可以更上一层楼,把component这个配置进行声明化,最终的配置如下://router-config.json[{path:"/goods",exact:true,title:"商品列表",component:"goods-list",children:[{path:"/goods/:id",exact:true,title:"商品详情",component:"goods-item"}]}]//router-config.tsximportReactfrom'react';importtype{RouterConfig}from'react-router-config';importrouterConfigfrom'./router-config.json';exportinterfacePathConfigextendsRouterConfig{menu:boolean;permission:string;childrenathConfig[];}exportinterfacePathConfigRawextendsPathConfig{component:string;childrenathConfigRaw[];}functionComponent(routerathConfigRaw[]):PathConfig[]{returnrouter.map(route=>{if(route.component){constLazyComponent=React.lazy(()=>import(`./pages/${route.component}`));route.component=();}if(Array.isArray(route.children)){route.children=Component(route.children);}returnroute;});}exportconstrouters=Component(routerConfig);将这些配置声明化,最大的好处是我们可以将其存储在后台配置中,通过后台菜单管理之类的功能对其进行各种管理配置。当然这种统一配置也不一定适合所有的场景,大家还是要具体问题具体分析。比如有同事和我反馈说微前端的场景里可能就不是特别合适,不管怎么统一配置,主应用和子应用中可能都需要分别存在一些配置。主应用需要菜单,子应用需要路由,这种时候可能稍微拆分一下反而更倒是合适的。参考资料[1]react-router-config: https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config[2]https://ant.design/components/menu-cn/
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-12 12:25 , Processed in 0.583129 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表