React后台系统前端路由鉴权

832 阅读5分钟

前言

React-Router是React生态里面很重要的一环 常用的路由模式有 BrowserRouterHashRouterMemoryRouter

现在React的单页应用的路由基本都是前端自己管理的,而不像以前是后端路由.

React管理路由的库常用的就是React-Router。

本文想写一下React-Router的使用,但是光介绍API又太平淡了,而且已经 官方文档 写得很好了,我这里就用一个常见的开发场景来看看React-Router是怎么用的吧。

我们一般的系统都会有用户访问权限的限制,某些页面可能需要用户具有一定的权限才能访问

菜单这块的权限 不同后台权限设计不一样, 本次就不往深沉次赘述了.

image.png

本文主要介绍菜单权限和组件权限的对接实现

版本

依赖版本
react@17.0.2
react-router-dom @5.2.0 
antd@4.15.4
auth-component@0.0.1

 路由鉴权

因为我们的路由是通过接口获取的, 所以我们需要做成可配置的 json 格式的数据, 类似如下:

{
  "hierarchyMenuList": [
    {
      "menuName": "查询",
      "menuUrl": "/comprehensive-query",
      "children": [
        {
          "menuName": "订单",
          "parentMenuName": "查询",
          "menuUrl": "/comprehensive-query/order-query",
          "children": null
        }
      ]
    }
  ]
}

拿到上面的数据我们需要通过 react-router-dom 对菜单的数据进行注册初始化路由, 方案如下:

// 这里使用了统一封装的sdk去通过接口获取权限数据并进行格式化
import { useAuthorityList } from 'auth-component';

const XRouterFC<Props> = (props: Props) => {
  // 首页地址 假如没有首页地址 获取可跳转的第一个菜单路由进行重定向
  const { redirectUrl } = props;
  // 获取菜单列表数据
  const authorityList = useAuthorityList();

  const filterRouter = useCallback(() => {
    if (authorityList.menuList.length === 0return [];

    const menuList = authorityList.menuList;

    // 格式化接口菜单数据合并进result用于生成子应用路由
    menuList.forEach((menu: MENU_INTERFACE) => {
        let componentany = () => '';
        result.push({
          name: menu.menuName,
          path: menu.menuUrl,
          component,
        });
    });

    // 生成路由列表组件
    return recursionRouter([...whiteRoutes, ...result]);
  }, [authorityList]);

  return (
     // 使用 Suspense + lazy + import(webpack) 做组件的懒加载
    <Suspense fallback={<div></div>}>
    // 使用 Switch 匹配组件
      <Switch>
        <Redirect exact from="/" to={redirectUrl} />
        // 渲染路由列表组件
        {filterRouter()}
        <Route path="*" component={NotFound} />
      </Switch>
    </Suspense>
  );
};

动态递归初始化路由 (简化版)

function recursionRouter({
  routeList,
  path = '',
}: {
  routeList: Routes[];
  path?: string;
}) {
  return (
    <>
      {routeList.map(route => {
        if (Array.isArray(route.children) && route.children.length > 0) {
          return (
            <Fragment key={route.path}>
              // 递归拍平成一维路由
              {recursionRouter({
                routeList: route.children,
                path: `${path}${route.path}`,
              })}
            </Fragment>
          );
        }

        if (route.component) {
          const Component = route.component;

          return (
            <Route
              key={route.path}
              exact
              strict
              // 路由路径的初始化
              path={path + route.path}
              render={props => <Component {...props} />}
            ></Route>
          );
        }
      })}
    </>
  );
}

至此, 我们获取后台中心并进行路由初始化的工作已经完成

有小伙伴可能存在嵌套路由的需求, 这里没有做这方面处理, 如果需要嵌套路由的话 实现思路差不多, 就是在动态生成路由的时候要做处理 假如说是 React-router@6 的话 我们还能使用 outlet 的方式去处理会更加方便

下面我们说下动态菜单初始化和匹配

菜单配置

React 后台菜单组件我一般使用 Ant-Design 的 Menu 导航菜单组件

const XMenu: FC = () => {
  const location = useLocation<Location>();

  // 菜单选中值
  const selectKeys: SelectKeys = useMemo<SelectKeys>(() => {
    return [location.pathname];
  }, [location.pathname]);

  return (
    <Menu
      className="h-full"
      theme="dark"
      mode="inline"
      defaultOpenKeys={routes
        .filter(route => Array.isArray(route.children))
        .map(route => route.path)}
      selectedKeys={selectKeys}
    >
      // 生成菜单 routes 通过接口获取菜单数据
      // 具体实现不过多赘述
      {recursionMenu(routes)}
    </Menu>
  );
};

 微前端接入鉴权

随着项目体量不断的扩大, 打包的速度和项目的部署时间会变长, 代码之间的功能可能会出现耦合, 为了优化上述问题, 可以考虑接入微前端进行项目优化, 我们这里使用 qiankun

在设计微前端如何接入的时候, 本着企业管理系统体量比较大, 配置优于代码的结论, 决定使用后台中心的菜单配置化接入微前端子应用.

因为后台中心没有现成的方案去配置子应用, 所以我们采用了菜单配置的方式去配置子应用的数据, 再在项目中过滤, 筛选出这部分数据用于初始化子应用

实现如下:

 1. 首先在后台中心配置微前端的数据

具体配置的方式大同小异, 维护一套微前端配置数据即可

 2. 在主项目中初始化
function init(micro) {
  const { NODE_ENV } = process.env;
  // 区分本地还是线上环境
  if (NODE_ENV === 'development') {
    require('./initFromLocal');
  } else {
    // micro.childList 就是筛选过滤的子应用配置数据
    if (Array.isArray(micro?.childList) && micro.childList.length > 0) {
      const initFormServer = require('./initFromServer');
      initFormServer.default(micro.childList);
    }
  }
}

初始化线上子应用配置

import { prefetchApps, registerMicroApps, start } from 'qiankun';

const getActiveRule = (path: any) => (location: any) =>
  location.pathname.indexOf(path) > -1;

function initFormServer(list: any) {
  const microList = formatList(list);
  // 格式化子应用配置数据
  const entryConfigRecord<
    string,
    {
      namestring;
      devstring;
      linestring;
    }
  > = microList.reduce((config: any, micro: any) => {
    micro.entry &&
      (config[micro.entry] = {
        name: micro.entry,
        dev`//localhost:${micro.devPort}`// 子应用本地地址
        line`${window.location.origin}${micro.line}`// 子应用线上地址(约定式)
      });

    return config;
  }, {});

  function getEntry(name: ENTRY_NAME) {
    return isDev ? entryConfig[name].dev : entryConfig[name].line;
  }

  registerMicroApps(
    microList.map((entry: any) => ({
      name`${entry.entry} microApp`// 子应用名称
      entrygetEntry(entry.entry), // 入口地址
      container: entry.container// 容器dom
      activeRulegetActiveRule(entry.activeRule), // 匹配规则
    }))
  );

  start();
}

页面鉴权

按钮权限的配置使用了后台中心的接口权限+页面权限配合

简单说就是配置一个 Json List 数据

可以配置到全局的状态管理中, 然后需要用到的地方去取出来控制页面功能的权限

微前端的话通过通讯的方式传递

总结

完成以上的配置后, 我们的项目鉴权基本就差不多了. 达到的效果如下

1. 后台中心配置, 控制菜单

2. 动态接入子应用+子应用菜单权限

3. 页面鉴权

本文内容比较简略,作为熟悉后台鉴权

思考

1. 假如说需要做递归嵌套路由权限控制, 该如何改造项目路由 期望效果如下

image.png

   我有一个左侧菜单 1, 右侧内容区域的右上角有一个下拉菜单 2, 右侧内容区域的左侧有个菜单 3

   json 伪代码如下:

 [{
    "name": "菜单1",
    "children": [{
       "name": "菜单2",
        "children": [{
          "name": "菜单3",
        }]
    }]
   }]