umi4搭建的管理后台框架(集成约定式路由, 自定义布局, 服务端获取菜单, 权限验证等功能)

2,621 阅读12分钟

概览

umi4搭建的轻量级开发框架, 集成约定式路由, 自定义布局(还有自定义路由渲染), 从服务端获取菜单, 以及全局状态管理和权限验证, 最新的依赖, 最棒的开发体验, 克隆即用, 项目地址: umi4-admin, 朋友们可以点个star, 以备不时之需

前言

年初的时候公司要开发一个管理后台的项目, 一开始使用的是antd pro, 但在使用自带布局/src/app.tsx的时候传headerRender设置自定义头部, 结果实际渲染出来的自定义头部position: fixed;在原本的头部上方, 将原本的头部覆盖了, 原来的头部还在, 我以为是替换, 没想到是覆盖在上面, 然后由于是固定定位, 导致自定义头部不但覆盖住了原来的头部, 还将左侧的菜单和中间的内容也覆盖住了, 需要去/src/global.less中写样式覆盖, 这让我百思不得其解, 对布局有一些高度定制化的需求因此最终使用了自定义布局的方案

antd pro全量区块不支持umi4, 想着umi4出来了, 肯定有比umi2 umi3优越的地方, 打算直接使用umi4

约定式路由是因为我从umi2开始就一直使用的约定式的路由, 配置式路由用起来比较繁琐, 各种配置

而至于数据流(状态管理)方案的选择, 主要还是因为自定义布局方案导致, 次要原因是我用dva也很久了, 觉得挺顺手, 但如果没有高度自定义的布局的需求, umi4官方提供的数据流方案是非常棒的, 轻量级的全局状态管理方案, 使用起来也很方便: 按照约定的方式书写代码, 以自定义hooks的形式创建store, 使用的时候用umi4提供的api: useModel就可以了, 还做了类似reselect的性能优化, 个人觉得这个方式摒弃了稍有门槛的redux的写法, 而是对用户侧做了一个收敛, 使得用户使用起来更方便, 也更易理解, 但依旧是我们所熟悉的flux的思想, 是flux思想的另一种实现, 这里要给umi团队一个大大的赞, 云谦大佬sorrycc, 虎哥xiaohuoni, 我尤其对虎哥的这个帖子印象深刻: 开发中遇到的问题,已经处理的,在这里记录一下。给朋友们一个参考, 想当年我刚开始用umi的时候是之前公司的一个大佬郭老师dxcweb推荐我用的, 而虎哥的这个帖子给了我很大的帮助, 瑞思拜

以及控制台打开看到antd的各种已经废弃的报错, 虽然不影响使用, 但多了很多不必要的error, 严重影响开发时候的调试工作, 这是由于antd废弃了一些api, 而项目中还在使用导致的, 这个问题修改起来比较繁琐, 同时还有上面提到的几个点, 于是就有了这个项目

接下来跟大家聊一聊项目中的一些重点和特点

环境变量

自定义环境变量应以UMI_APP_开头, 并写到.env中, 这样才能在代码中通过process.env.UMI_APP_xxx访问到, 项目中的.env文件目前只有一个值: UMI_APP_BASEURL=/, 克隆之后需要在项目中创建一个.env文件并在其中写入UMI_APP_BASEURL=/

如果不想以UMI_APP_开头, 则需要在.env中写完之后在配置的define中做配置, 比如:

.env:

domain=http://example.com

/config/config.ts:

//...
define: {
  "process.env": {
    domain: process.env.domain
  }
}
//...

这样项目中才能访问到process.env.domain, 以及如果配置中的define做了如上那样process.env的配置, 那所有环境变量(包括以UMI_APP_开头的环境变量)都要配置到其中, 不然访问process.env的时候将只能访问到define中配置的值, 因为这样配置之后process.env被覆盖了

更便捷的配置可以这样来:

//...
define: {
  "process.env": process.env
}
//...

这样所有环境变量都能通过process.env访问了, 无论是自带的还是自定义的

有需要的朋友还可以看看这两个issue:

无法配置自定义的环境变量

config中设置define后,命令行中设置的环境变量在app.tsx中无法找到

以及, 关于是否应该提交.env文件和是否应该有多个.env文件的问题可以看看这两个描述:

Should I commit my .env file?

Should I have multiple .env files?

个人觉得也不应该提交.env以及只有一个.env即可, 因为里面的配置提交到库中不安全, 同时每个部署环境都有不同的配置, 协作开发的话单独发即可

但这个情况也不绝对, 需要在不同环境中使用不同配置, 推荐通过umi自带的环境变量UMI_ENV来完成, 详情可以查看官方文档: UMI_ENV, 也可以结合这两个来看:

umi_env 目前好像是覆盖方式,可以支持合并方式吗

请问umi4还支持多config目录下多环境配置吗?

以及config目录下的多环境配置我试了下暂时不行, 也可能是我姿势不对: 请问umi4还支持多config目录下多环境配置吗?#discussioncomment-4807605, 根目录下.umirc.ts的我也没能成功进行多环境的配置, 了解用法的朋友希望能不吝赐教

布局

舍弃了自带的/src/app.tsx布局, 转而使用自定义的布局: /src/layouts/index.tsx, 这个方式比较符合我这边项目的需求, 而且issue里面看到也有不少小伙伴有需求, 同时也是因为没使用自带的layout, 404页面需要自己实现一下, 这个比较简单, 就不展开了

自定义布局中做自定义渲染

在布局中有些根据路由信息做自定义渲染的需求可以看看这个: 自定义layout组件,props里拿不到config的routes,没办法自己实现菜单的渲染?, 其中我个人也回复了一下, 大意是使用useLocation来实现, 这是它的官方文档: useLocation_React Router, 项目中我也是这么处理的

这里除了登录页不走/src/layouts之外, 我还做了额外的处理: 当用户已经登录, 此时如果再次访问登录页(比如用户手动输入或者通过书签进入登录页)会做重定向到非登录页(有redirect则重定向到redirect, 没有则到首页)的操作, 具体代码可以查看这个文件: /src/components/LayoutWrapper.tsx

路由

使用的是约定式路由. 路由功能的提供, umi4使用的是react-router6, 官方文档是这个: React Router, 关于约定式路由的嵌套问题可以看这个: 约定式路由无法生成嵌套路由!!

以及具体哪一条路由有效, 是由菜单接口返回的数据决定的, 菜单接口返回的数据会显示在左侧菜单栏中, 当一条路由(菜单数据)被接口返回了, 也就是由接口提供了, 那它就是有效的, 但由于使用了约定式路由, 只有当这条路由同时还在项目目录中被创建了, 它才能正常渲染

默认情况下, 一条路由哪怕菜单接口没提供, 但在项目中被创建了, 那它也能被正常渲染, 只是左侧菜单栏中就无法显示了, 但这不符合逻辑: 一个用户能访问的路由应该在该用户登录之后由菜单接口返回, 并且在菜单栏中显示(一些需要在菜单栏中隐藏的菜单除外), 除去需要隐藏在菜单栏中的菜单之外, 其他没在菜单栏中显示的菜单表示该用户无法访问, 即使是项目中创建了, 也就是说此时手动输入url或者更常见的是通过收藏栏访问都无法访问, 都应该显示404, 而这个功能项目中也做了处理, 简单来说就是:

  • 菜单中的数据: 用来显示到左侧菜单栏中, 表示当前登录用户所能访问的页面, 菜单数据中没有的页面, 哪怕实际存在但都会显示404, 因为菜单数据中没有表示该页面无法被当前登录用户访问到
  • 项目中的目录(约定式路由): 通过目录和文件及其命名分析出路由配置从而使得路由能正常渲染页面, 目录数量要>=菜单数据, 这才能保证菜单能符合预期地渲染或者不渲染

当然了, 还有一种情况就是菜单接口返回了某条路由, 而项目中没有对应的页面, 此时也是404, 这是umi4自带的404功能, 这里只能处理项目中有, 而菜单数据中有(渲染页面)或者没有(渲染404页面)的情况

另外登录页的路由不需要菜单接口返回(不然登录页就会显示在左侧菜单位置了), 登录页建好就行, 它不走路由判断逻辑(因为它不由菜单接口返回), 具体的路由判断跳转的逻辑可以查看/src/utils/handleRedirect.ts

菜单

菜单由服务端返回, 也是存到全局状态中, 返回的数据的结构要是Menu能消费的ItemType, 同时不再包含access字段, 当前用户的菜单就是当前用户能访问的了, 只是页面内的操作不全是当前用户都能操作的, 页面鉴权主要是防止当前用户打开其他用户的路由(比如打开了其他用户存的书签)这样的情况, 权限内容在后面有描述, 以及菜单的ts定义如下:

/**
 * 菜单项
 * @description id 数据库中数据的id
 * @description pid 数据库中数据的id(父级的id)
 * @description key 菜单项的唯一标志, 使用string类型代替React.Key: 
 * https://ant.design/components/menu-cn#itemtype, 不然会出现key类型不对导致的菜单项无法被选中的问题
 * @description lable 菜单的标题
 * @description hideInMenu 在菜单中隐藏
 * @description path 路由路径,
 * 有无children的菜单都会有这个字段, 无children的菜单跳转这个值, 有children的跳redirect,
 * 因为有children表示这个菜单是可展开的, 此时有children的path只是表示它的一个位置, 而非真正有效的路由
 * @description redirect 重定向路由路径,
 * 只有有children的菜单有, 当这个菜单的children中有可选中的菜单时, 这个值为第一个可选中的菜单的path,
 * 当这个菜单的children中没可以选中的菜单, 而是还有children时,
 * 该值就是它children中的children的第一个可选中的菜单的path,
 * 就是无论如何, 这个值都是第一个有效路由, 具体可看mock数据中的菜单数据
 * 以及这个字段理论上来说应该是可选的字段, 但为了让后端容易处理, 这里写成固定有的字段,
 * 在不需要这个字段的数据中后端返回空串即可
 * @description children 子菜单
 */
type MenuItem = {
  id: number;
  pid?: number;
  key: string;
  path: string;
  redirect: string;
  hideInMenu?: boolean;
  label: React.ReactElement | string;
  children?: MenuItem[];
}

权限

这个地方是个重点, 同时也是一个需要自己实现的地方, 因为自带的权限控制需要initial-state, 而这个initial-state又依赖自带的/src/app.tsx布局, 刚好这里使用的是自定义的布局, 因此最终只能自己实现

这个逻辑在后端自然是RBAC的方案, 而前端关注的主要则是具体的权限, 具体逻辑如下:

  1. 前后端约定每个页面的权限, 这里包括页面访问权限, 就是路由的权限和页面内各个操作元素的权限, 并在页面上写好, 代码里是写在authority.ts中(以object的形式定义), 以_开头是因为这样才不会被算作一个路由
  2. 后端返回当前登录用户的所有权限(类型是string[]), 前端取到之后和authority.ts中的做对比, 从而达到鉴权的目的

权限我分成了页面和页面内元素的权限, 具体代码在这: 页面权限: /src/components/PageAccess.tsx, 页面内元素的权限: /src/components/Access.tsx, 前端权限的声明, 页面权限在这: /src/pages/authority.ts, 各个页面内权限写在各个页面的目录中, 比如: /src/pages/about/m/authority.ts

页面权限需要根据不同的路由来决定, 因此它的类型定义如下:

/**
 * 页面权限类型
 * @description key是路由path, value是权限数组
 */
type PageAuthority = {
  [path: string]: string[];
}

而页面内元素的权限又有所不同, 一个个元素, 需要一个个独立的权限, 它的类型定义如下:

/**
 * 权限类型
 * @description key是权限名称, value是具体的权限字符串
 */
type Authority = {
  [key: string]: string;
}

参考umi4access_umi4文档自己实现了一个鉴权的组件:

  1. 页面权限: /src/components/PageAccess.tsx
    1. 有权限: 正常渲染页面(children)
    2. 没权限: 返回result_antd组件的403结果

页面鉴权的处理放到了/src/layouts/index.tsx中, 因为这个组件是所有需要做鉴权处理的页面的父级, 在这处理最合适不过了

  1. 页面内部: /src/components/Access.tsx
    1. 有权限: 正常渲染元素(children)
    2. 没权限:
      1. fallback: 什么都不渲染
      2. fallback: 渲染fallback

页面内权限的处理需要使用<Access />组件在各个页面中单独处理

登录之后后端返回的用户信息和用户权限都放到全局状态也就是dva

其他

另外还有Git工作流, Mock, 代理, 请求, lint这些功能, 具体的可以到项目的git仓库中查看: umi4-admin

参考文献:

  1. umi4脚手架生成的simple, antd pro项目模板以及umi4的文档: umi4
  2. antd pro的脚手架工具@ant-design/pro-cli生成的umi3antd pro项目, 选umi4提示无法安装全部区块, 因此这里我选的是umi3, 以及antd pro的文档: antd pro
  3. 2018, 2019年使用umi2, umi3搭建的项目以及umi3的文档: umi3
  4. procomponents的文档: procomponents