1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        統(tǒng)一路由、菜單、面包屑和權限配置

        共 8049字,需瀏覽 17分鐘

         ·

        2021-12-16 20:44

        我最近做的一個新項目是一個典型的中后臺項目,采用的是 React + React Router + Antd 方案。正常情況下我們需要定義路由配置,在頁面中定義面包屑的數(shù)據(jù),頁面寫完之后需要在左側菜單中增加頁面的路由。寫多了之后,我會覺得同一個路由的相關信息在不同的地方重復聲明,實在是有點麻煩,為什么我們不統(tǒng)一在一個地方定義,然后各個使用的地方動態(tài)獲取呢?

        單獨配置

        首先我們看看每個功能單獨定義是如何配置的,之后我們再總結規(guī)律整理成一份通用的配置。

        路由和權限

        路由我們使用了react-router-config[1]進行了聲明化的配置。

        //?router.ts
        import?{?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.tsx
        import?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/default
        import?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.ts
        import?{?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/default
        import?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.tsx
        import?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.ts
        import?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.ts
        import?{?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/default
        import?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.tsx
        import?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?????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.tsx
        import?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
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品老女人 | chinagay男性吹潮教程 | 亚洲激情成人五月天 | 99精品在线观看视频 | 国产又大又粗又长硬又紧又爽的视频 | 亚洲性爱电影院 | 琪琪色在线观看 | 国产又黄又粗视频 | 国产午夜亚洲精品午夜鲁丝片 | 暗呦网一区二区三区 |