mingjiezhou / notes

web开发相关的笔记
26 stars 4 forks source link

基于 vue-antd-admin 开发项目后的几点感想(二):异步路由与菜单 #5

Open mingjiezhou opened 2 years ago

mingjiezhou commented 2 years ago

前言

上一篇 讲了使用 vue-antd-admin 开发中台项目后的一些感想,并没有提到其动态路由的配置,主要是因为当时觉得其文档写的比较详细了,而且当时并没有遇到什么坑,没想到今天接了一个需求,需要开发一套消息系统,其中有一个显示全部消息的页面,我按照官方文档中的方法配置了页面路由后,发现和我想要的效果不同,其直接全屏显示了,而我需要的是显示在侧边栏菜单的右侧内容部分,于是仔细看了文档和源码后,花了亿点点时间解决了这个需求,并且产出了本篇文章。

路由与菜单

我长话短说,基本的使用一定要看官方文档,概述起来就是说,vue-antd-admin 项目也是完全依赖 vue-router 并使用其配置规则,其提供了同步、异步两种路由方案:

同步的非常简单,就是我们熟悉的vue-router 的配置, 示例代码可以看这个 src/router/config.js就不讲了;

关键是异步,首先需要本地配置路由的map 文件,也就是把所有的完整路由拆分成单个的路由配置进行注册, 文件在这里 /router/async/router.map.js

然后根据接口返回的路由配置与map 文件结合生成最终的路由,接口返回的路由格式如下

[{
  router: 'root',                           //匹配 router.map.js 中注册名 registerName = root 的路由
  children: [                               //root 路由的子路由配置
    {
      router: 'dashboard',                  //匹配 router.map.js 中注册名 registerName = dashboard 的路由
      children: ['workplace', 'analysis'],  //dashboard 路由的子路由配置,依次匹配 registerName 为 workplace 和 analysis 的路由
    },
    {
      router: 'form',                       //匹配 router.map.js 中注册名 registerName = form 的路由
      children: [                           //form 路由的子路由配置
        'basicForm',                        //匹配 router.map.js 中注册名 registerName = basicForm 的路由
        'stepForm',                         //匹配 router.map.js 中注册名 registerName = stepForm 的路由
        {
          router: 'advanceForm',            //匹配 router.map.js 中注册名 registerName = advanceForm 的路由
          path: 'advance'                   //重写 advanceForm 路由的 path 属性
        }
      ]   
    },
    {
      router: 'basicForm',                  //匹配 router.map.js 中注册名 registerName = basicForm 的路由
      name: '验权表单',                     //重写 basicForm 路由的 name 属性
      icon: 'file-excel',                   //重写 basicForm 路由的 icon 属性
      authority: 'form'                     //重写 basicForm 路由的 authority 属性
    }
  ]
}]

重点来了,异步路由是不能覆盖所有的路由的,比如404、login 之类的页面是不需要配置到接口权限里的,所有我们需要一个基础路由配置文件,里面注册的路由都可以merge 到最终的路由配置中。

文件地址在这里 /router/async/config.async.js

官方文档中基础路由的配置使用方法如下:

const routesConfig = [
  'login',                      //匹配 router.map.js 中注册的 registerName = login 的路由
  'root',                       //匹配 router.map.js 中注册的 registerName = root 的路由
  {
    router: 'exp404',           //匹配 router.map.js 中注册的 registerName = exp404 的路由
    path: '*',                  //重写 exp404 路由的 path 属性
    name: '404'                 //重写 exp404 路由的 name 属性
  },
  {
    router: 'exp403',           //匹配 router.map.js 中注册的 registerName = exp403 的路由
    path: '/403',               //重写 exp403 路由的 path 属性
    name: '403'                 //重写 exp403 路由的 name 属性
  }
]

如果按照此方法配置,最终的路由文件大概是这样子

截屏2022-02-21 17 24 30

可以看到 消息中心是和跟路由平级的,此时的效果是这样的

5fde398711b54cd390ed8f563612012b_tplv-k3u1fbpfcp-watermark

看来我需要把 notification 路由给添加到首页下的children 属性里,其才会在侧边栏生成新的菜单,我修改一下配置方法试试

然后发现,/首页下的children 中并没有这个路由,也就是说其并没有别merge 到一起;问题出在 路由merge 函数里,这个函数在这里 src/utils/routerUtil.js loadRoutes 中的

// loadRoutes 函数中的这行是关键
const finalRoutes = mergeRoutes(basicOptions.routes, routes)

那么看下它是怎么定义的

/**
 * 合并路由
 * @param target {Route[]} 本地基础路由
 * @param source {Route[]} 接口异步路由
 * @returns {Route[]}
 */
function mergeRoutes(target, source) {
  const routesMap = {}
  target.forEach(item => routesMap[item.path] = item)
  source.forEach(item => {
    routesMap[item.path] = item
  })
  return Object.values(routesMap)
}

可以看到,其是一个浅拷贝函数,同名属性会被覆盖掉,额看来我需要改造一下写一个深度拷贝函数,突然我发现 mergeRoutes 函数下紧接着就有一个定义好的 深度合并路由函数!

/**
 * 深度合并路由
 * @param target {Route[]}
 * @param source {Route[]}
 * @returns {Route[]}
 */
function deepMergeRoutes(target, source) {
  // 映射路由数组
  const mapRoutes = routes => {
    const routesMap = {}
    routes.forEach(item => {
      routesMap[item.path] = {
        ...item,
        children: item.children ? mapRoutes(item.children) : undefined
      }
    })
    return routesMap
  }
  console.log(target, source)
  const tarMap = mapRoutes(target)
  const srcMap = mapRoutes(source)

  // 合并路由
  const merge = _merge(srcMap, tarMap)

  // 转换为 routes 数组
  const parseRoutesMap = routesMap => {
    return Object.values(routesMap).map(item => {
      if (item.children) {
        item.children = parseRoutesMap(item.children)
      } else {
        delete item.children
      }
      return item
    })
  }
  return parseRoutesMap(merge)
}

可以看到其是用深度优先的递归方式来进行merge 操作的; 现在我把 mergeRoutes 替换为 deepMergeRoutes 尝试下发现真的成功了!

0e53f837aa88417383ba46d1fba9a5c9_tplv-k3u1fbpfcp-watermark

虽然在官方文档中并没有看到这个函数的身影,但是作者显然考虑到了这种需求,这样,异步路由的方案使用起来就更方便了。

对了,官方还在 routes 的元数据属性 meta 中注入了三个属性 icon、invisible 和 page,其中 invisible 将控制其是否显示在侧边栏菜单中,这个很有用。

总结

今天通过一个具体的需求探究,介绍了 vue-antd-admin 的动态路由方案,本来我还觉得它不太好用,今天无意中发现了 deepMergeRoutes 函数,通过它本地基础路由的配置方便很多,现在看来这个异步动态路由方案还是挺不错的,点赞。


合集:我的 github 博客及案例源代码