統(tǒng)一路由、菜單、面包屑和權限配置前端Sharing關注共 8049字,需瀏覽 17分鐘 ·2021-12-16 20:44 我最近做的一個新項目是一個典型的中后臺項目,采用的是 React + React Router + Antd 方案。正常情況下我們需要定義路由配置,在頁面中定義面包屑的數(shù)據(jù),頁面寫完之后需要在左側菜單中增加頁面的路由。寫多了之后,我會覺得同一個路由的相關信息在不同的地方重復聲明,實在是有點麻煩,為什么我們不統(tǒng)一在一個地方定義,然后各個使用的地方動態(tài)獲取呢?單獨配置首先我們看看每個功能單獨定義是如何配置的,之后我們再總結規(guī)律整理成一份通用的配置。路由和權限路由我們使用了react-router-config[1]進行了聲明化的配置。//?router.tsimport?{?RouteConfig?}?from?'react-router-config';import?DefaultLayout?from?'./layouts/default';import?GoodsList?from?'./pages/goods-list';import?GoodsItem?from?'./pages/goods-item';export?const?routes:?RouteConfig[]?=?[??{????component:?DefaultLayout,????routes:?[??????{????????path:?'/goods',????????exact:?true,????????title:?'商品列表',????????component:?GoodsList,??????},??????{????????path:?'/goods/:id',????????exact:?true,????????title:?'商品詳情',????????component:?GoodsItem,??????}????],??},];//app.tsximport?React?from?'react';import?{?BrowserRouter?as?Router?}?from?'react-router-dom';import?{?renderRoutes?}?from?'react-router-config';import?{?routes?}?from?'./router';export?default?function?App()?{??return?{renderRoutes(routes)}</Router>;};菜單左側導航菜單我們使用的是[2]組件,大概的方式如下://./layouts/defaultimport?React?from?'react';import?{?renderRoutes?}?from?'react-router-config';import?{?Layout,?Menu?}?from?'antd';export?default?function({route})?{??return?(??????????????????Header??????</Layout.Header>??????????????????????????????????????????????????oods">商品列表????????????????????????????????????????????????{renderRoutes(route.routes)}????????????????????);}權限這里的權限主要指的是頁面的權限。我們會請求一個服務端的權限列表接口,每個頁面和功能都對應一個權限點,后臺配置后告知我們該用戶對應的權限列表。所以我們只需要記錄每個頁面對應的權限點,并在進入頁面的時候判斷下對應的權限點在不在返回的權限列表數(shù)據(jù)中即可。而頁面權限與頁面是如此相關,所以我們慣性的會將頁面的權限點與頁面路由配置在一塊,再在頁面統(tǒng)一的父組件中進行權限點的判斷。//?router.tsimport?{?RouteConfig?}?from?'react-router-config';import?DefaultLayout?from?'./layouts/default';import?GoodsList?from?'./pages/goods-list';import?GoodsItem?from?'./pages/goods-item';export?const?routes:?RouteConfig[]?=?[??{????component:?DefaultLayout,????routes:?[??????{????????path:?'/goods',????????exact:?true,????????title:?'商品列表',????????component:?GoodsList,????????permission:?'goods',??????},??????{????????path:?'/goods/:id',????????exact:?true,????????title:?'商品詳情',????????component:?GoodsItem,????????permission:?'goods-item',??????}????],??},];//?./layouts/defaultimport?React,?{?useEffect,?useMemo?}?from?'react';import?{?useHistory,?useLocation?}?from?'react-router-dom';import?{?matchRoutes?}?from?'react-router-config';export?default?function({route})?{??const?history?=?useHistory();??const?location?=?useLocation();??const?page?=?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.tsximport?React?from?'react';import?{?Link?}?from?'react-router-dom';import?{?Breadcrumb?}?from?'antd';export?default?function()?{??return?(??????????????????"/goods">商品列表</Link>??????Breadcrumb.Item>??????商品詳情</Breadcrumb.Item>????Breadcrumb>??);}合并配置通過上面的整理我們可以看到,所有的功能都是和配置相關,所有的配置都是對應路由的映射。雖然路由本身是平級的,但由于菜單和面包屑屬于多級路由關系,所有我們的最終配置最好是多級嵌套,這樣可以記錄層級關系,生成菜單和面包屑比較方便。最終我們定義的配置結構如下://router-config.tsimport?type?{?RouterConfig?}?from?'react-router-config';import?GoodsList?from?'./pages/goods-list';import?GoodsItem?from?'./pages/goods-item';export?interface?PathConfig?extends?RouterConfig?{??menu?:?boolean;??permission?:?string;??children?:?PathConfig[];}export?const?routers?=?[??{????path:?'/goods',????exact:?true,????title:?'商品列表',????component:?GoodsList,????children:?[??????{????????path:?'/goods/:id',????????exact:?true,????????title:?'商品詳情',????????component:?GoodsItem??????}????]??}];路由基于上面的嵌套配置,我們需要定義一個flatRouters()方法將其進行打平,替換原來的配置即可。//router.tsimport?{?RouteConfig?}?from?'react-router-config';import?DefaultLayout?from?'./layouts/default';import?{?routers,?PathConfig?}?from?'./router-config';function?flatRouters(routers:?PathConfig[]):?PathConfig[]?{??const?results?=?[];??for?(let?i?=?0;?i?????const?{?children,?...router?}?=?routers[i];????results.push(router);????if?(Array.isArray(children))?{??????results.push(...routeFlat(children));????}??}??return?results;}export?const?routes:?RouteConfig[]?=?[??{????component:?DefaultLayout,????routes:?flatRouters(routers),??},];菜單菜單本身也是嵌套配置,將其正常渲染出來即可。//./layouts/defaultimport?React?from?'react';import?{?renderRoutes?}?from?'react-router-config';import?{?Layout,?Menu?}?from?'antd';const?NavMenu:?React.FC<{}>?=?()?=>?(??"inline">????{routers.filter(({?menu?})?=>?menu).map(({?title,?path,?children?})?=>?(??????Array.isArray(children)?&&?children?.filter(({?menu?})?=>?menu).length???(??????????????????{children.filter(({?menu?})?=>?menu).map(({?title,?path?})?=>?(??????????????????????))}????????</Menu.SubMenu>??????)?:?(????????>??????)????))}??</Menu>);const?NavMenuItem:?React.FC<{path:?string,?title:?string}>?=?({path,?title})?=>?(??????{/^https?:///.test(path)???(??????"_blank"?rel="noreferrer?noopener">{title}</a>????)?:?(??????{title}Link>????)}??</Menu.Item>);export?default?function({route})?{??return?(??????????????????Header??????Layout.Header>????????????????????????????????</Layout.Sider>??????????????????{renderRoutes(route.routes)}????????Layout.Content>??????</Layout>????Layout>??);};面包屑面包屑的難點在于我們需要根據(jù)當前頁面路由,不僅找到當前路由,還需要找到它的各種父級路由。除了定義一個findCrumb()方法來查找路由之外,為了方便查找,還在配置上做了一些約定。如果兩個路由是父子關系,那么他們的路由路徑也需要是包含關系。例如商品列表和商品詳情是父子路由關系,商品列表的路徑是/goods,那么商品詳情的路由則應該為/goods/:id。這樣在遞進匹配查找的過程中,只需要判斷當前頁面路由是否包含該路徑即可,減小了查找的難度。另外還有一個問題大家可能會注意到,商品詳情的路由路徑是/goods/:id,由于帶有命名參數(shù),當前路由去做字符串匹配的話肯定是沒辦法匹配到的。所以需要對命名參數(shù)進行正則通配符化,方便做路徑的匹配。命名參數(shù)除了影響路徑查找之外,還會影響面包屑的鏈接生成。由于帶有命名參數(shù),我們不能在面包屑中直接使用該路徑作為跳轉路由。為此我們還需要寫一個stringify()方法,通過當前路由獲取到所有的參數(shù)列表,并對路徑中的命名參數(shù)進行替換。這也是為什么之前我們需要將父子路由的路徑定義成包含關系。子路由在該條件下肯定會包含父級路徑中所需要的參數(shù),極大的方便我們父級路由的生成。//src/components/breadcrumb.tsximport?React,?{?useMemo?}?from?'react';import?{?Breadcrumb?as?OBreadcrumb,?BreadcrumbProps?}?from?'antd';import?{?useHistory,?useLocation,?useParams?}?from?'react-router';import?Routers,?{?PathConfig?}?from?'../router-config';function?findCrumb(routers:?PathConfig[],?pathname:?string):?PathConfig[]?{??const?ret:?PathConfig[]?=?[];??const?router?=?routers.filter(({?path?})?=>?path?!==?'/').find(({?path?})?=>????new?RegExp(`^${path.replace(/:[a-zA-Z]+/g,?'.+?').replace(///g,?'\/')}`,?'i').test(pathname)??);??if?(!router)?{?return?ret;?}??ret.push(router);??if?(Array.isArray(router.children))?{????ret.push(...findCrumb(router.children,?pathname));??}??return?ret;}function?stringify(path:?string,?params:?Record)?{??return?path.replace(/:([a-zA-Z]+)/g,?(placeholder,?key)?=>?params[key]?||?placeholder);}const?Breadcrumb?=?React.memo(props?=>?{??const?history?=?useHistory();??const?params?=?useParams();??const?location?=?useLocation();??const?routers:?PathConfig[]?=?useMemo(????()?=>?findCrumb(Routers,?location.pathname).slice(1),????[location.pathname]??);??if?(!routers.length?||?routers.length?2)?{????return?null;??}??const?data?=?props.data???props.data?:?routers.map(({?title:?name,?path?},?idx)?=>?({????name,????onClick:?idx?!==?routers.length?-?1???()?=>?history.push(stringify(path,?params))?:?undefined,??}));??return?(??????????{data.map(({name,?onClick})?=>?(??????????????????{name}span>????????</Breadcrumb.Item>??????))}????OBreadcrumb>??);});export?default?Breadcrumb;后記至此我們的統(tǒng)一配置基本上就屢清楚了,我們發(fā)現(xiàn)只是簡單的增加了幾個屬性,就讓所有的配置統(tǒng)一到了一起。甚至我們可以更上一層樓,把component這個配置進行聲明化,最終的配置如下://router-config.json[??{????path:?"/goods",????exact:?true,????title:?"商品列表",????component:?"goods-list",????children:?[??????{????????path:?"/goods/:id",????????exact:?true,????????title:?"商品詳情",????????component:?"goods-item"??????}????]??}]//router-config.tsximport?React?from?'react';import?type?{?RouterConfig?}?from?'react-router-config';import?routerConfig?from?'./router-config.json';export?interface?PathConfig?extends?RouterConfig?{??menu?:?boolean;??permission?:?string;??children?:?PathConfig[];}export?interface?PathConfigRaw?extends?PathConfig?{??component?:?string;??children?:?PathConfigRaw[];}function?Component(router:?PathConfigRaw[]):?PathConfig[]?{??return?router.map(route?=>?{????if(route.component)?{??????const?LazyComponent?=?React.lazy(()?=>?import(`./pages/${route.component}`));??????route.component?=?(????????"loading...">??????????????????</React.Suspense>??????);????}????if(Array.isArray(route.children))?{??????route.children?=?Component(route.children);????}????return?route;??});}export?const?routers?=?Component(routerConfig);將這些配置聲明化,最大的好處是我們可以將其存儲在后臺配置中,通過后臺菜單管理之類的功能對其進行各種管理配置。當然這種統(tǒng)一配置也不一定適合所有的場景,大家還是要具體問題具體分析。比如有同事和我反饋說微前端的場景里可能就不是特別合適,不管怎么統(tǒng)一配置,主應用和子應用中可能都需要分別存在一些配置。主應用需要菜單,子應用需要路由,這種時候可能稍微拆分一下反而更倒是合適的。參考資料[1]react-router-config: https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config[2]https://ant.design/components/menu-cn/ 瀏覽 112點贊 評論 收藏 分享 手機掃一掃分享分享 舉報 評論圖片表情視頻評價全部評論推薦 在字節(jié)是如何實現(xiàn)統(tǒng)一路由、菜單、面包屑和權限配置的程序員成長指北0如何設計路由權限?勾勾的前端世界0Vue 路由權限控制SegmentFault0vue-router 源碼和動態(tài)路由權限分配前端迷0淺析 vue-router 源碼和動態(tài)路由權限分配前端勸退師0和路由和路由由中移物聯(lián)網(wǎng)公司打造,以滿足用戶需求,支持外設擴展為設計理念,具備極簡配置、故障診斷、遠程APP控制、照片分享、應用接入等功能特點,力爭為用戶打造一個溫馨的智能家居環(huán)境。具備可視化的圖形管理界面淺析 vue-router 源碼和動態(tài)路由權限分配程序源代碼0和路由和路由由中移物聯(lián)網(wǎng)公司打造,以滿足用戶需求,支持外設擴展為設計理念,具備極簡配置、故障診斷、遠程APVue路由權限控制分析前端達人0面包屑面包屑0點贊 評論 收藏 分享 手機掃一掃分享分享 舉報