016 Umi 项目中的菜单与权限

4,329 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

上节课中我们完成了页面的大致布局的编写,今天我们主要把重点放在菜单配置中,为什么这么一个简单的菜单配置要单独写一篇文章来说明呢?

因为他在后续的“动态菜单”,权限校验等环节都有很重要的作用。

首先你要先把菜单数据上升到页面级数据,虽然它只是一个组件,但是它里面的数据需要和页面数据(主要是路由)关联上,几乎所有的导航组件,都需要有这一点的意识。

首先我们需要获取到当前项目的所有路由信息,这有两种方式,一种是配置式,自己整理出一个清单,每增加一个页面都更新这个清单,有个好处就是后续菜单交由服务端管控的时候,可以直接将这份数据给他。坏处就是页面数是固定的,后续想做动态菜单,有点困难,因为你配置的路由信息需要是一个“最大值”,否则未配置的页面没有被引用则不会被编译。

另一种就是约定式的,新建一个页面,即增加一个菜单信息,我们通过 Umi 提供的 API 获取到最新的页面路由信息,调用一些工具类,将他们转换成菜单数据,后面维护心智很低。约定式的方式,所有的页面都会被构建,只是通过菜单加权限来控制页面是否可访问,可以实现类似动态路由这样的需求。缺点就是需要独立维护一份菜单数据,主要是页面名称的“翻译文档“。比如首页 ”/home“ 在菜单中应该显示 “首页”。

获取当前页面数据

Umi@4 中要获取页面配置非常的简单,只需要使用 useAppData 即可,它返回全局的应用数据。

declare function useAppData(): {
  routes: Record<id, Route>;
  routeComponents: Record<id, Promise<React.ReactComponent>>;
  clientRoutes: ClientRoute[];
  pluginManager: any;
  rootElement: string;
  basename: string;
  clientLoaderData: { [routeKey: string]: any };
  preloadRoute: (to: string) => void;
};

routesclientRoutes 这两个数据都是路由数据,前者是对象,以 pathnamekey,以 parentId 来标记层级和嵌套关系。后者是一个数组,以 children 来表示树形结构。

const routes = {
    'a':{
        parentId: "b"
        path: "a"
    },
    'b':{
        path: "b"
    },
} 
const clientRoutes = [{
    path: "b",
    children:[{
        path: "a"
    }]
}]

以上两个数据“对等”。

所以我们要取到当前的所有的路由配置信息,则

import { useAppData } from "umi";

const App = ()=>{
    const { clientRoutes } = useAppData();
    const { children } = clientRoutes[0];

}

将路由转化成菜单数据

const clientRoutes = [{
    path: "b",
    children:[{
        path: "a"
    }]
}];

// 转化为

const menuData = [{
    key:"/b",
    icon:<PieChartOutlined />,
    label:"首页",
    children:[{
        key:"/a",
        icon:<UserOutlined />,
        label:"用户",
    }]
}];

通过观察分析,我们发现,其实路由数据中,我们只有 pathchildren 数据有用,而菜单数据中,我们还需要 iconlabel ,这时候就需要引入我们前面提到的 翻译文档 了。

const menuHash: any = {
  "/": {
    label: "首页",
    icon: <PieChartOutlined />,
  },
  user: {
    label: "用户",
    icon: <UserOutlined />,
  },
};

至此我们的 路由转菜单的工具类 为:

const getItem = (path: string, children?: MenuItem[]) => {
  const route = menuHash[path];
  return {
    key: path.startsWith("/") ? path : `/${path}`,
    icon: route?.icon || <></>,
    children,
    label: route?.label || path,
  } as MenuItem;
};

const routesToMenu = (routes: any[]): MenuItem[] => {
  return routes
    .map((route) => {
      const { path, children } = route;
      if (children) {
        return getItem(path, routesToMenu(children));
      }
      return getItem(path);
    });
};

运行项目,访问 http://127.0.0.1:8888/

16menu01.jpg

这是你会发现,菜单中有很多我们之前写的 demo 页面,我们并不想让他们展示出来。所以我们需要增加一个访问权限的黑名单。

const unaccessible = ["/hooks", "/useEffect", "/usemodel", "/useState"];

只要简单的修改一下,我们的 routesToMenu 方法即可。

const routesToMenu = (routes: any[]): MenuItem[] => {
  return routes
    .filter((i) => {
      const path = i.path.startsWith("/") ? i.path : `/${i.path}`;
      return !unaccessible.includes(path);
    })
    .map((route) => {
      const { path, children } = route;
      if (children) {
        return getItem(path, routesToMenu(children));
      }
      return getItem(path);
    });
};

保存代码,你讲看到菜单中只有两个数据了。

增加页面权限

但是这只是将路由入口隐藏了,如果用户知道你的路由信息,比如此时我们直接当问 http://127.0.0.1:8888/usemodel,虽然菜单已经过滤了但是我们依旧可以直达页面。

16menu02.jpg

其实原理也很简单,只要判断当前页面 pathname 在我们的不可访问清单就返回 403 页面即可,这个要看你们项目中的权限采用的是黑名单模式还是白名单模式了,黑名单模式匹配上拦截,白名单模式匹配上放行。

  import { Result, Button } from "antd";

  if (unaccessible.includes(location.pathname)) {
    return (
      <Result
        status="403"
        title="403"
        subTitle="抱歉,你没有权限访问这个页面!"
        extra={
          <Button type="primary" onClick={() => navigate(-1)}>
            返回上一个页面
          </Button>
        }
      />
    );
  }

16menu03.jpg

菜单与路由跳转

需要实现的功能,点击菜单触发路由跳转,当前页面对应的菜单项需要高亮显示。

import { useAppData, useNavigate, useLocation } from "umi";

const App = ()=>{
  const navigate = useNavigate();
  const location = useLocation();
  const { clientRoutes } = useAppData();
  const { children } = clientRoutes[0];
  const items = routesToMenu(children);

  return <Menu
    theme="dark"
    onClick={(e) => {
      navigate(e?.key);
    }}
    defaultSelectedKeys={[location.pathname]}
    mode="inline"
    items={items}
  />;
}

16menu04.jpg

至此,我们的菜单与权限部分的所有功能都开发完毕。这里面需要引申到项目的权限管控上,将 unaccessible 和用户的登录信息关联上,就可以了。如果有面试官问你,你们项目中的权限部分是怎么做的?如果用户知道你的页面url是否可以直接访问页面,如何拦截?你应该可以回答的很明白了。当然这节课只是为了讲明白原理,在实际项目开发中我们可以用上 layout 和 access 插件的组合来更合理的完成权限和菜单。

源码归档