PanJiaChen/vue-element-admin

请问路由配置可以动态的改变吗?

reuwi opened this issue · 94 comments

reuwi commented

我们现在想要增加一个功能,就是管理员可以更改每个菜单的可访问角色列表,我想在进入页面之前从服务器获取每个路由的可访问列表并更改,请问这样子可以实现吗?

只要将 @/router/index.js 中的 asyncRouterMap 存在服务端就可以,原理和现有逻辑是差不多的,后续可能会改成可配置的形式。
Duplicate of #167
后续可关注该issue

reuwi commented

请问可以详细的指点下吗?

将 @/router/index.js 中的 asyncRouterMap 存在服务端

大概应该怎样实现呢?因为asyncRouterMap还跟components相关联,因此不太理解应该怎样实现,非常感谢~

再做一个asyncRouterMap.components的name 和 本地components 做一个映射

const map={
 login:require('login/index').default // 同步的方式
 login:()=>import('login/index')      // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
 { path: '/login', component: 'login', hidden: true }
]
//之后遍历这个map,动态生成asyncRouterMap
并将 component 替换为map[component]
reuwi commented

非常感谢,按照您的方法已经成功实现了~

reuwi commented

@PanJiaChen 请问asyncRouterMap在服务器端怎么存取比较好?目前我们公司的后台给的解决方案是把name和role作为两个字段存在数据库中,其它字段作为一个payload存起来,获取asyncRouterMap时候再拼出来,这样子的实现感觉很奇怪,请问有什么好的实现方法吗?

reuwi commented

目前角色管理页就是用树状表格来实现的
_20180118095844

name 和 role作为存储的关键词也没什么问题,选择自己合适的交互方式就行了,反正前端总归需要洗数据的。

@gaoshijun1993 能提供怎么实现后台动态权限的实现么?后端人员确实不知道前端怎么整理。

其实和上面处理方式差不多。
1,/router/index.js 里面的asyncRouterMap相当于本地所有的菜单数据,每级别必须配置meta: {title:"ify",roles: [] }, children: [{meta: {title:"ify",roles: [] }}] 相当于本地默认的这个都是没有权限的。 除了公共显示的,不需要配置roles
2,在合并路由之前:GenerateRoutes 这个方法: 通过后台返回配置的权限表,合并进去

 //后台返回的权限表 :结构和本地的asyncRouterMap层级一致
        let roleTypes = [{
          meta: {
            title: 'example',
            roles: ['admin', 'editor']
          },
          children: [{
            meta: {
              title: 'createArticle',
              roles: []
            }
          }]
        }, {
          meta: {
            title: 'table',
            roles: ['admin', 'editor']
          },
          children: [{
            meta: {
              title: 'dynamicTable',
              roles: ['admin', 'editor']
            }
          }, {
            meta: {
              title: 'complexTable',
              roles: ['editor']
            }
          }]
        }]
     
//判断是否是管理员
      if (roles.indexOf('admin') >= 0) {
        //动态给所有权限
        for (let j = 0; j < asyncRouterMap_copy.length; j++) {
          if ('roles' in asyncRouterMap_copy[j]['meta']) {
            asyncRouterMap_copy[j]['meta']['roles'] = ['admin'];
          }
          if ('children' in asyncRouterMap_copy[j]) {
            for (let k = 0; k < asyncRouterMap_copy[j]['children'].length; k++) {
              asyncRouterMap_copy[j]['children'][k]['meta']['roles'] = ['admin'];
            }
          }
        }
      } else {
        //如果不是全局配置的管理员
        //判断后端正确返回的路由表
        if (roleTypes.length <= 0) {
          asyncRouterMap_copy = []
        } else {
          for (let j = 0; j < asyncRouterMap_copy.length; j++) {
            for (let i = 0; i < roleTypes.length; i++) {
              //遍历父级
              if (roleTypes[i]['meta']['title'] === asyncRouterMap_copy[j]['meta']['title']) {
                asyncRouterMap_copy[j]['meta']['roles'] = roleTypes[i]['meta']['roles']
              }

              //遍历子级
              if ('children' in asyncRouterMap_copy[j]) {
                for (let k = 0; k < asyncRouterMap_copy[j]['children'].length; k++) {

                  if ('children' in roleTypes[i]) {
                    for (let e = 0; e < roleTypes[i]['children'].length; e++) {
                      if (asyncRouterMap_copy[j]['children'][k]['meta']['title'] === roleTypes[i]['children'][e]['meta']['title']) {
                        asyncRouterMap_copy[j]['children'][k]['meta']['roles'] = roleTypes[i]['children'][e]['meta']['roles']
                      }
                    }
                  }
                }
              }


            }
          }
        }
      }

前端本地是有一份完整的菜单结构,但是初始化的时候,都不给权限,然后后端只返回权限类型就可以了。

如果能解决路由中:component: () => import ('@/views/permission/page'), 这个由后台返回的话解析的话,其实整个asyncRouterMap都可以由后端返回。

其实真正开发的时候,新加一个菜单功能,配置component这个必然前端要做修改的。所以我认为放一份全部的菜单在前端是合理的。

我这里只是做了两级的一个判断,有需要大家可以写个递归。

@PanJiaChen
大佬,你看看现在的两种方式,是把数据存在前端本地多一点,还是存在后端服务器好一点。
我看了上面的实现方式实际就是所有的异步获取的路由数据,component是本地有一份对应的列表。

@baidan 还是看需求,简单项目权限比较固定,权限角色比较少的情况下前端控制一下就好了。

举个例子,我现在在维护的一个项目,刚开始就十个左右权限角色,每个权限角色对应的权限和页面也很固定。但随着迭代,角色和页面越来越多,之间的划分就变得很麻烦了。所以现在做成了,权限表存在后端,前端提供一个配置每个权限角色能访问的页面的可视化页面。让产品或者专门的人来维护它。程序员不要再思考每个页面需要什么权限了。

@PanJiaChen 我觉得把每个页面的权限 从原来的角色 改成为 权限字段 即可 , 每次登陆的时候 根据当前的用户的角色, 获取当前角色的 权限列表信息 比如 [user:browse, log:delete] 这种 然后在来判断 这个权限列表 对应了哪些菜单 或者 页面按钮 是可访问的。

这样做有如下好处

  • 后端 只用维护 这个角色 对应的哪些api权限

  • 前端 可单独维护自己的菜单页面 以及 这个页面只用 被哪些权限可访问

相当于 与角色解耦了。

前后端都很轻松了 去除了 有些方案中产生的 菜单列表 这种 api ,以及对应产生 要对菜单列表数据 进行router 转换。

因为asyncRouterMap还跟components相关联,有试了一下把整个asyncRouterMap转成json存到后端,然后分析components发现非常复杂,很难像以前新增菜单一样直接在数据库添加一条信息就可以了,前端的router还得写
所以采用转化整个router(只保留必要的属性)生成一棵树,然后保存到后端,如果要获取菜单权限时,只从后台获取name跟前端router的映射做一次匹配
image
image
image
前端很少接触,所以不知道前端的深拷贝这么折腾,自己写了一个应急用一下,才发现大神在utils里已经有个方法了,差点吐血==
写得不好,希望可以对权限有需要的朋友一个思路的参考

HoHo1 commented

因为asyncRouterMap还跟components相关联,有试了一下把整个asyncRouterMap转成json存到后端,然后分析components发现非常复杂,很难像以前新增菜单一样直接在数据库添加一条信息就可以了,前端的router还得写
所以采用转化整个router(只保留必要的属性)生成一棵树,然后保存到后端,如果要获取菜单权限时,只从后台获取name跟前端router的映射做一次匹配
image
image
image
前端很少接触,所以不知道前端的深拷贝这么折腾,自己写了一个应急用一下,才发现大神在utils里已经有个方法了,差点吐血==
写得不好,希望可以对权限有需要的朋友一个思路的参考

请问GenerateRoutes方法里面的 allFunction.then()的allFunction是哪里的

因为asyncRouterMap还跟components相关联,有试了一下把整个asyncRouterMap转成json存到后端,然后分析components发现非常复杂,很难像以前新增菜单一样直接在数据库添加一条信息就可以了,前端的router还得写
所以采用转化整个router(只保留必要的属性)生成一棵树,然后保存到后端,如果要获取菜单权限时,只从后台获取name跟前端router的映射做一次匹配
image
image
image
前端很少接触,所以不知道前端的深拷贝这么折腾,自己写了一个应急用一下,才发现大神在utils里已经有个方法了,差点吐血==
写得不好,希望可以对权限有需要的朋友一个思路的参考

请问GenerateRoutes方法里面的 allFunction.then()的allFunction是哪里的

是从后端获取下来的当前页面的所有方法,塞到一个数组里面,原来根据各方法的状态来实现对界面按钮的控制

求教一下,动态路由在component赋值的时候遇到的问题。

我的asyncmap路由表整个是从后台拼好返回的。由于component要求是对象。
于是做了一次遍历

   -- route.component = ()=>import(route.component)  //这么处理了以后,在跳转的时候会报一个async webpack xxx的  err ,加载不到组件

   ++ route.component =asyncRouterMap_Map[route.name]  //按您说的  Map[name] 映射的方式是可以的。

请教一下为什么第一种方式不行。

求教一下,动态路由在component赋值的时候遇到的问题。

我的asyncmap路由表整个是从后台拼好返回的。由于component要求是对象。
于是做了一次遍历

   -- route.component = ()=>import(route.component)  //这么处理了以后,在跳转的时候会报一个async webpack xxx的  err ,加载不到组件

   ++ route.component =asyncRouterMap_Map[route.name]  //按您说的  Map[name] 映射的方式是可以的。

请教一下为什么第一种方式不行。

自答一下,是不是因为在我请求的时候已经编译成js文件了,再拼进去已经不走webpack了,所以没法找到组件。而第二种方式是在run dev的时候已经把需要的组件找到了。。。

请问下映射的方式,必须在本地存储全部的路由表吗?component动态生成,报找不到对应的模块

可不可以不管角色,直接就从后台返回路由表,返回什么就展示什么;也不做token验证,后台存SECCON,通过返回的状态码决定要不要跳向登录页,请问这种怎么改呢,谢谢。

/src/store/modules/permission.js
image

新增
import { routerMap } from '@/router/index'
/**

  • 递归过滤异步路由表,返回符合用户角色权限的路由表

  • @param routes asyncRouterMap

  • @param roles
    */
    function filterAsyncServerRolesRouter(roles) {
    const res = []
    roles.forEach(role => {
    const tmp = { ...role }
    var tmpComponent = tmp.component
    tmp.component = routerMap[tmpComponent]

    if (tmp.children) {
    tmp.children = filterAsyncServerRolesRouter(tmp.children)
    }
    res.push(tmp)
    })
    return res
    }

文件:/src/router/index.js
//参数对应component
export const routerMap = {
studentList: () => import('@/views/v1/users-manage/student-list'), Layout: () => import('@/views/layout/Layout')
}
服务器端必须返回有children参数的路由,才能显示
例如:
{
path: '/users',
component: Layout,
redirect: '/users/teacher-list',
name: 'UserManage',
alwaysShow: false,
meta: {
title: 'userManage',
icon: 'example'
},
children: [
{
path: 'teacher-list',
component: () => import('@/views/v1/users-manage/teacher-list'),
name: 'teacherList',
meta: { title: 'teacherList', icon: 'list' }
}
]
}
服务器端返回数据
[
{
"path": "/teacher",
"component": "Layout",
"name": "UserManage",
"alwaysShow": true,
"meta": {
"title": "studentList",
"icon": "list"
},
"children": [
{
"path": "/student-list",
"component": "studentList",
"name": "StudentList",
"hidden": false,
"meta": {
"title": "studentList",
"icon": "list"
}
}
]
},
{
"path": "/student-list",
"component": "studentList",
"name": "StudentList",
"hidden": false,
"meta": {
"title": "studentList",
"icon": "list"
}
}
]

我提了一个PR,写了演示 #1597

重新单独提了一个PR,分离了iframe #1600

重新单独提了一个PR,分离了iframe #1600

如果有多个角色呢

vue-cli3.0的版本的,现在可以后端实现返回权限,前端根据后端返回的权限显示相应的菜单路由了吗?

8pig commented

vue-cli3.0的版本的,现在可以后端实现返回权限,前端根据后端返回的权限显示相应的菜单路由了吗?

#1600

后端存的路由表是所有理由都要存上去吗?404这些页面的路由也要存上去吗?

有没有详细点的说明啊?关于这个路由权限的功能。

有没有详细点的说明啊?关于这个路由权限的功能。

可以的,前端你写一个路由表是全部的,包括所有的对应关系,然后根据后台来的数据,比如path,或者是id,进行筛选匹配就可以了

@anrananran nest2Flag(),flat2Nest()这两个方法能给一下吗?感谢感谢

@anrananran 哈哈 谢谢你 大佬

请问一下动态配置路由后, 刷新页面成白页面是什么情况? 能否指点一下

微信截图_20190710210835
路由设置为后端接口返回的动态路由后,页面变成一片空白,也不报错,看见好几个人遇到这个问题了,也没人回答,,有一个说是父级component也要引入,这个我也引入了,问题依旧存在,请问大佬们,该怎么破?

请问一下动态配置路由后, 刷新页面成白页面是什么情况? 能否指点一下

router.addRoutes(newRouter)后需要next({ ...to, replace: true })
参考:https://segmentfault.com/q/1010000015875314

附一个我完成的PHP的动态方案,主要代码

首先PHP要返回用户可访问的路由,期望的结果是菜单中可以有一个指向 /example/sample 的菜单和路由。

public function actionMenu()
    {
        $data =[
            [
                'path' => '/example',
                'component' => 'Layout',
                'redirect'=> '',
                'name'=> 'ExampleRoot',
                'meta'=> [
                    'title'=>'示例',
                    'icon'=>'table'
                ],
                'children'=>[
                    [
                        'path' => 'sample',
                        'component' => '/example/sample',
                        'name'=> 'ExampleSample',
                        'meta'=> [
                            'title'=>'example',
                            'icon'=>'table'
                        ],
                    ]
                ]
            ]
        ]
            
            ;

        return json_encode($data);
    }

然后修改 src/store/modules/permission.js

添加一个方法

function dataArrayToRoutes(data) {
  const res = []
  data.forEach(item => {
    const tmp = { ...item }
    if (tmp.component === 'Layout') {
      tmp.component = Layout
    } else {
      let sub_view = tmp.component
      sub_view = sub_view.replace(/^\/*/g, '')
      tmp.component = () => import(`@/views/${sub_view}`)  //这里很重要,把view动态加载进来,而且似乎我只找到这样的写法,用拼接不行,然后 views 后面没有斜杆也不行
    }
    if (tmp.children) {
      tmp.children = dataArrayToRoutes(tmp.children)
    }
    res.push(tmp)
  })
  return res
}

然后修改这个文件中的 actions

const actions = {
  generateRoutes({ commit, state }, { roles, menus }) {
    return new Promise(resolve => {
      let accessedRoutes
      // if (roles.includes('admin')) {
      //   accessedRoutes = asyncRoutes || []
      // } else {
      //   accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      // }
      // commit('SET_ROUTES', accessedRoutes)

      accessedRoutes = dataArrayToRoutes(menus)

      commit('SET_ROUTES', accessedRoutes)

      resolve(accessedRoutes)
    })
  }
}

然后新增一个 user/getMenus 的 Vuex 的 action

const state = {
  token: getToken(),
  name: '',
  avatar: '',
  introduction: '',
  roles: [],
  menus: [] //这个是我新增的
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_INTRODUCTION: (state, introduction) => {
    state.introduction = introduction
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  },
  SET_MENUS: (state, menus) => { //这里是新增的
    state.menus = menus
  }
}

const actions = {
    getMenus({ commit, state }) { //这个是新增的action
    return new Promise((resolve, reject) => {
      getMenus(state.token).then(response => {  //这里的getMenus是调用request方法从服务端获得路由菜单数据的Promise,类似getInfo
        const { data } = response
        console.log(data)

        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const menus = data

        // roles must be a non-empty array
        if (!menus || menus.length <= 0) {
          reject('getMenus: menus must be a non-null array!')
        }

        commit('SET_MENUS', menus)
        resolve(menus)
      }).catch(error => {
        reject(error)
      })
    })
  },
}

还有一个文件是 src/permission.js
要在 store.dispatch('permission/generateRoutes') 代码附近要改一下,原先从本地配置+roles获得用户路由,现在改从服务端获得之后再 addRoutes

          const menus = await store.dispatch('user/getMenus')
          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', { menus })
          // dynamically add accessible routes
          router.addRoutes(accessRoutes)

亲测可行……

你们有没有考虑过 菜单路由 的耦合问题。
后端输出的是菜单的结构,当菜单层级多于两个时候,按照上面各位的常用做法(就是按照菜单的层级来生成路由配置),假设层级数为3,那需要在层级为2的页面里写<router-view/>,第3层的页面才能显示。当然有时候是有这种需求的,没问题。但是,以我个人的需求来说,绝大部分情况下,我都是希望菜单是3层,而路由是两层(也就是第2和第3层都是显示在Layout的<router-view/>里)。

如果我说的有问题,还请大佬请出来。

暂时的想法是,给菜单数据一个“路由是否跟父菜单同级”的属性,比如为sameRouteLevelAsParent
当一个菜单item,它没有子菜单(isEmpty(item.children) === true),而且item.sameRouteLevelAsParent === true,那就把它的路由跟父级菜单的路由放在同一级。

如果颗粒化到按钮的控制、除了v-permission 指令,还有更好的解决方式嘛

我这边的思路是,前台代码保留一份完整的路由信息表,服务器返回带有name和roles的对应数据即可,前端负责将数据合并,然后加载“动态路由”。
亲测没有问题!
上代码

后台返回数据 mock\role\routes.js

export const asyncRoutes = [
  {
    path: '/managener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'funManagener',
    name: 'FunManagener',
    meta: {
      title: '回收系统功能管理',
      icon: 'tree',
      roles: ['admin']
    }
  },

  {
    path: 'acManagener',
    name: 'AcManagener',
    meta: {
      title: '回收系统账套管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenModule',
    name: 'ScreenModule',
    meta: {
      title: '可视化大屏模板管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenAccount',
    name: 'ScreenAccount',
    meta: {
      title: '可视化大屏账号管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentnewPlate',
    name: 'AcMangentnewPlate',
    meta: {
      title: ' 新增平台',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentdetail/:id',
    name: 'AcMangentdetail',
    meta: {
      title: ' 详情',
      roles: ['admin']
    }
  },

  {
    path: 'acMangentedit/:id',
    name: 'AcMangentedit',
    meta: {
      title: ' 编辑',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentepeizhi/:id',
    name: 'AcMangentepeizhi',
    meta: {
      title: ' 配置功能 ',
      roles: ['admin']
    }
  },
  {
    path: '/system',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: ['admin']
    }
  },

  {
    path: 'rolesManagener',
    name: 'RolesManagener',
    meta: { title: '用户权限管理', icon: 'tree', roles: ['admin'] }
  },

  {
    path: 'userManagener',
    name: 'UserManagener',
    meta: { title: '用户管理', icon: 'table', roles: ['admin'] }
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台数据 src\router\index.js

export const asyncRoutes = [
  {
    path: '/managener',
    component: Layout,
    redirect: '/managener/acManagener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'funManagener',
        name: 'FunManagener',
        component: () =>
                 import('@/views/managener/funManagener/index'),
        meta: { title: '回收系统功能管理', icon: 'tree' }
      },
      {
        path: 'acManagener',
        name: 'AcManagener',
        component: () =>
                 import('@/views/managener/acManagener/index'),
        meta: { title: '回收系统账套管理', icon: 'table' }
      },
      {
        path: 'screenModule',
        name: 'ScreenModule',
        component: () =>
                 import('@/views/managener/screenModule/index'),
        meta: { title: '可视化大屏模板管理', icon: 'table' }
      },
      {
        path: 'screenAccount',
        name: 'ScreenAccount',
        component: () =>
                 import('@/views/managener/screenAccount/index'),
        meta: { title: '可视化大屏账号管理', icon: 'table' }
      },
      {
        path: 'acMangentnewPlate',
        name: 'AcMangentnewPlate',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/newPlate/index'),
        meta: {
          title: ' 新增平台',
          icon: 'Rest',
          roles: []
        }
      },
      {
        path: 'acMangentdetail/:id',
        name: 'AcMangentdetail',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/acdetail/index'),
        meta: {
          title: ' 详情',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentedit/:id',
        name: 'AcMangentedit',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/editplate/index'),
        meta: {
          title: ' 编辑',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentepeizhi/:id',
        name: 'AcMangentepeizhi',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/peizhi/index'),
        meta: {
          title: ' 配置功能 ',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      }
    ]
  },
  {
    path: '/system',
    component: Layout,
    redirect: '/system/rolesManagener',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'rolesManagener',
        name: 'RolesManagener',
        component: () =>
                 import('@/views/system/rolesManagener/index'),
        meta: { title: '用户权限管理', icon: 'tree' }
      },
      {
        path: 'userManagener',
        name: 'UserManagener',
        component: () =>
                 import('@/views/system/userManagener/index'),
        meta: { title: '用户管理', icon: 'table' }
      }
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台处理代码 src\store\modules\permission.js

import { asyncRoutes, constantRoutes } from '@/router'
import { getAsyncRoutes } from '../../api/role'
import { deepClone } from '../../utils/index'

const clientRoutes = deepClone(asyncRoutes)
/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  async generateRoutes({ commit }, roles) {
    let PermissionRouters = await getAsyncRoutes().then(res => {
      const data = res.data
      PermissionRouters = makePermissionRouters(data, clientRoutes)
      // console.log('api:' + PermissionRouters)
      return PermissionRouters
    })
    console.log(PermissionRouters)
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        console.log(PermissionRouters)

        accessedRoutes = PermissionRouters || []
      } else {
        accessedRoutes = filterAsyncRoutes(PermissionRouters, roles)
      }
      console.log(accessedRoutes)

      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

动态路由成功了,但是在页面内刷新会404 是什么原因

动态路由成功了,但是在页面内刷新会404 是什么原因

动态添加路由的时候最后一个添加404页面

不知道有没有遇到 router.addRoutes不生效的,贴出我的解决方法
store.dispatch('permission/generateRoutes', menus).then(() => {
router.options.routes=store.getters.routes;
router.addRoutes(store.getters.routes);
next({...to, replace: true})
}
)

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

我这边的思路是,前台代码保留一份完整的路由信息表,服务器返回带有name和roles的对应数据即可,前端负责将数据合并,然后加载“动态路由”。
亲测没有问题!
上代码

后台返回数据 mock\role\routes.js

export const asyncRoutes = [
  {
    path: '/managener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'funManagener',
    name: 'FunManagener',
    meta: {
      title: '回收系统功能管理',
      icon: 'tree',
      roles: ['admin']
    }
  },

  {
    path: 'acManagener',
    name: 'AcManagener',
    meta: {
      title: '回收系统账套管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenModule',
    name: 'ScreenModule',
    meta: {
      title: '可视化大屏模板管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenAccount',
    name: 'ScreenAccount',
    meta: {
      title: '可视化大屏账号管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentnewPlate',
    name: 'AcMangentnewPlate',
    meta: {
      title: ' 新增平台',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentdetail/:id',
    name: 'AcMangentdetail',
    meta: {
      title: ' 详情',
      roles: ['admin']
    }
  },

  {
    path: 'acMangentedit/:id',
    name: 'AcMangentedit',
    meta: {
      title: ' 编辑',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentepeizhi/:id',
    name: 'AcMangentepeizhi',
    meta: {
      title: ' 配置功能 ',
      roles: ['admin']
    }
  },
  {
    path: '/system',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: ['admin']
    }
  },

  {
    path: 'rolesManagener',
    name: 'RolesManagener',
    meta: { title: '用户权限管理', icon: 'tree', roles: ['admin'] }
  },

  {
    path: 'userManagener',
    name: 'UserManagener',
    meta: { title: '用户管理', icon: 'table', roles: ['admin'] }
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台数据 src\router\index.js

export const asyncRoutes = [
  {
    path: '/managener',
    component: Layout,
    redirect: '/managener/acManagener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'funManagener',
        name: 'FunManagener',
        component: () =>
                 import('@/views/managener/funManagener/index'),
        meta: { title: '回收系统功能管理', icon: 'tree' }
      },
      {
        path: 'acManagener',
        name: 'AcManagener',
        component: () =>
                 import('@/views/managener/acManagener/index'),
        meta: { title: '回收系统账套管理', icon: 'table' }
      },
      {
        path: 'screenModule',
        name: 'ScreenModule',
        component: () =>
                 import('@/views/managener/screenModule/index'),
        meta: { title: '可视化大屏模板管理', icon: 'table' }
      },
      {
        path: 'screenAccount',
        name: 'ScreenAccount',
        component: () =>
                 import('@/views/managener/screenAccount/index'),
        meta: { title: '可视化大屏账号管理', icon: 'table' }
      },
      {
        path: 'acMangentnewPlate',
        name: 'AcMangentnewPlate',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/newPlate/index'),
        meta: {
          title: ' 新增平台',
          icon: 'Rest',
          roles: []
        }
      },
      {
        path: 'acMangentdetail/:id',
        name: 'AcMangentdetail',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/acdetail/index'),
        meta: {
          title: ' 详情',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentedit/:id',
        name: 'AcMangentedit',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/editplate/index'),
        meta: {
          title: ' 编辑',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentepeizhi/:id',
        name: 'AcMangentepeizhi',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/peizhi/index'),
        meta: {
          title: ' 配置功能 ',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      }
    ]
  },
  {
    path: '/system',
    component: Layout,
    redirect: '/system/rolesManagener',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'rolesManagener',
        name: 'RolesManagener',
        component: () =>
                 import('@/views/system/rolesManagener/index'),
        meta: { title: '用户权限管理', icon: 'tree' }
      },
      {
        path: 'userManagener',
        name: 'UserManagener',
        component: () =>
                 import('@/views/system/userManagener/index'),
        meta: { title: '用户管理', icon: 'table' }
      }
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台处理代码 src\store\modules\permission.js

import { asyncRoutes, constantRoutes } from '@/router'
import { getAsyncRoutes } from '../../api/role'
import { deepClone } from '../../utils/index'

const clientRoutes = deepClone(asyncRoutes)
/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  async generateRoutes({ commit }, roles) {
    let PermissionRouters = await getAsyncRoutes().then(res => {
      const data = res.data
      PermissionRouters = makePermissionRouters(data, clientRoutes)
      // console.log('api:' + PermissionRouters)
      return PermissionRouters
    })
    console.log(PermissionRouters)
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        console.log(PermissionRouters)

        accessedRoutes = PermissionRouters || []
      } else {
        accessedRoutes = filterAsyncRoutes(PermissionRouters, roles)
      }
      console.log(accessedRoutes)

      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

PermissionRouters = makePermissionRouters(data, clientRoutes) 后向下不执行了

现在登录接口出错,我想跳过登录的路由守卫直接进入,现在进去了,可是左侧的导航栏全是空白是为什么呢

求教一下,动态路由在component赋值的时候遇到的问题。

我的asyncmap路由表整个是从后台拼好返回的。由于component要求是对象。
于是做了一次遍历

   -- route.component = ()=>import(route.component)  //这么处理了以后,在跳转的时候会报一个async webpack xxx的  err ,加载不到组件

   ++ route.component =asyncRouterMap_Map[route.name]  //按您说的  Map[name] 映射的方式是可以的。

请教一下为什么第一种方式不行。

自答一下,是不是因为在我请求的时候已经编译成js文件了,再拼进去已经不走webpack了,所以没法找到组件。而第二种方式是在run dev的时候已经把需要的组件找到了。。。

你好,可以看一下解决的完整代码吗?我也遇到了同样的问题

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

我这边的思路是,前台代码保留一份完整的路由信息表,服务器返回带有name和roles的对应数据即可,前端负责将数据合并,然后加载“动态路由”。
亲测没有问题!
上代码

后台返回数据 mock\role\routes.js

export const asyncRoutes = [
  {
    path: '/managener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'funManagener',
    name: 'FunManagener',
    meta: {
      title: '回收系统功能管理',
      icon: 'tree',
      roles: ['admin']
    }
  },

  {
    path: 'acManagener',
    name: 'AcManagener',
    meta: {
      title: '回收系统账套管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenModule',
    name: 'ScreenModule',
    meta: {
      title: '可视化大屏模板管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenAccount',
    name: 'ScreenAccount',
    meta: {
      title: '可视化大屏账号管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentnewPlate',
    name: 'AcMangentnewPlate',
    meta: {
      title: ' 新增平台',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentdetail/:id',
    name: 'AcMangentdetail',
    meta: {
      title: ' 详情',
      roles: ['admin']
    }
  },

  {
    path: 'acMangentedit/:id',
    name: 'AcMangentedit',
    meta: {
      title: ' 编辑',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentepeizhi/:id',
    name: 'AcMangentepeizhi',
    meta: {
      title: ' 配置功能 ',
      roles: ['admin']
    }
  },
  {
    path: '/system',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: ['admin']
    }
  },

  {
    path: 'rolesManagener',
    name: 'RolesManagener',
    meta: { title: '用户权限管理', icon: 'tree', roles: ['admin'] }
  },

  {
    path: 'userManagener',
    name: 'UserManagener',
    meta: { title: '用户管理', icon: 'table', roles: ['admin'] }
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台数据 src\router\index.js

export const asyncRoutes = [
  {
    path: '/managener',
    component: Layout,
    redirect: '/managener/acManagener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'funManagener',
        name: 'FunManagener',
        component: () =>
                 import('@/views/managener/funManagener/index'),
        meta: { title: '回收系统功能管理', icon: 'tree' }
      },
      {
        path: 'acManagener',
        name: 'AcManagener',
        component: () =>
                 import('@/views/managener/acManagener/index'),
        meta: { title: '回收系统账套管理', icon: 'table' }
      },
      {
        path: 'screenModule',
        name: 'ScreenModule',
        component: () =>
                 import('@/views/managener/screenModule/index'),
        meta: { title: '可视化大屏模板管理', icon: 'table' }
      },
      {
        path: 'screenAccount',
        name: 'ScreenAccount',
        component: () =>
                 import('@/views/managener/screenAccount/index'),
        meta: { title: '可视化大屏账号管理', icon: 'table' }
      },
      {
        path: 'acMangentnewPlate',
        name: 'AcMangentnewPlate',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/newPlate/index'),
        meta: {
          title: ' 新增平台',
          icon: 'Rest',
          roles: []
        }
      },
      {
        path: 'acMangentdetail/:id',
        name: 'AcMangentdetail',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/acdetail/index'),
        meta: {
          title: ' 详情',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentedit/:id',
        name: 'AcMangentedit',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/editplate/index'),
        meta: {
          title: ' 编辑',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentepeizhi/:id',
        name: 'AcMangentepeizhi',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/peizhi/index'),
        meta: {
          title: ' 配置功能 ',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      }
    ]
  },
  {
    path: '/system',
    component: Layout,
    redirect: '/system/rolesManagener',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'rolesManagener',
        name: 'RolesManagener',
        component: () =>
                 import('@/views/system/rolesManagener/index'),
        meta: { title: '用户权限管理', icon: 'tree' }
      },
      {
        path: 'userManagener',
        name: 'UserManagener',
        component: () =>
                 import('@/views/system/userManagener/index'),
        meta: { title: '用户管理', icon: 'table' }
      }
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台处理代码 src\store\modules\permission.js

import { asyncRoutes, constantRoutes } from '@/router'
import { getAsyncRoutes } from '../../api/role'
import { deepClone } from '../../utils/index'

const clientRoutes = deepClone(asyncRoutes)
/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  async generateRoutes({ commit }, roles) {
    let PermissionRouters = await getAsyncRoutes().then(res => {
      const data = res.data
      PermissionRouters = makePermissionRouters(data, clientRoutes)
      // console.log('api:' + PermissionRouters)
      return PermissionRouters
    })
    console.log(PermissionRouters)
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        console.log(PermissionRouters)

        accessedRoutes = PermissionRouters || []
      } else {
        accessedRoutes = filterAsyncRoutes(PermissionRouters, roles)
      }
      console.log(accessedRoutes)

      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

PermissionRouters = makePermissionRouters(data, clientRoutes) 后向下不执行了

有没有人遇到过动态路由过来了,但是刷新页面就404了

qihb commented

请问一下动态配置路由后, 刷新页面成白页面是什么情况? 能否指点一下

在router.beforeEach里面使用router.addRoutes会有刷新空白的情况,改成router.onReady里面使用router.addRoutes。

求教一下,动态路由在component赋值的时候遇到的问题。

我的asyncmap路由表整个是从后台拼好返回的。由于component要求是对象。
于是做了一次遍历

   -- route.component = ()=>import(route.component)  //这么处理了以后,在跳转的时候会报一个async webpack xxx的  err ,加载不到组件

   ++ route.component =asyncRouterMap_Map[route.name]  //按您说的  Map[name] 映射的方式是可以的。

请教一下为什么第一种方式不行。

自答一下,是不是因为在我请求的时候已经编译成js文件了,再拼进去已经不走webpack了,所以没法找到组件。而第二种方式是在run dev的时候已经把需要的组件找到了。。。

大佬,请教下,您是怎么处理从后端返回的动态路由表的?我也是从后端返回拼接好的动态路由。

image

  1. 先把本地路由嵌套数组,进行平铺,做成一个Map集合
    image
    image
    image
    image

2.如上把从服务端请求回来的路由数据,与本地数据进行映射,主要是为了实现将 component 替换为 map[component]
image
3.permission.js路由守卫中使用router.addRoutes加入你的动态路由

yyggg commented

我想知道, 还有 api 里面的请求url呢,怎么跟菜单路由paht映射?

附一个我完成的PHP的动态方案,主要代码

首先PHP要返回用户可访问的路由,期望的结果是菜单中可以有一个指向 /example/sample 的菜单和路由。

public function actionMenu()
    {
        $data =[
            [
                'path' => '/example',
                'component' => 'Layout',
                'redirect'=> '',
                'name'=> 'ExampleRoot',
                'meta'=> [
                    'title'=>'示例',
                    'icon'=>'table'
                ],
                'children'=>[
                    [
                        'path' => 'sample',
                        'component' => '/example/sample',
                        'name'=> 'ExampleSample',
                        'meta'=> [
                            'title'=>'example',
                            'icon'=>'table'
                        ],
                    ]
                ]
            ]
        ]
            
            ;

        return json_encode($data);
    }

然后修改 src/store/modules/permission.js

添加一个方法

function dataArrayToRoutes(data) {
  const res = []
  data.forEach(item => {
    const tmp = { ...item }
    if (tmp.component === 'Layout') {
      tmp.component = Layout
    } else {
      let sub_view = tmp.component
      sub_view = sub_view.replace(/^\/*/g, '')
      tmp.component = () => import(`@/views/${sub_view}`)  //这里很重要,把view动态加载进来,而且似乎我只找到这样的写法,用拼接不行,然后 views 后面没有斜杆也不行
    }
    if (tmp.children) {
      tmp.children = dataArrayToRoutes(tmp.children)
    }
    res.push(tmp)
  })
  return res
}

然后修改这个文件中的 actions

const actions = {
  generateRoutes({ commit, state }, { roles, menus }) {
    return new Promise(resolve => {
      let accessedRoutes
      // if (roles.includes('admin')) {
      //   accessedRoutes = asyncRoutes || []
      // } else {
      //   accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      // }
      // commit('SET_ROUTES', accessedRoutes)

      accessedRoutes = dataArrayToRoutes(menus)

      commit('SET_ROUTES', accessedRoutes)

      resolve(accessedRoutes)
    })
  }
}

然后新增一个 user/getMenus 的 Vuex 的 action

const state = {
  token: getToken(),
  name: '',
  avatar: '',
  introduction: '',
  roles: [],
  menus: [] //这个是我新增的
}

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_INTRODUCTION: (state, introduction) => {
    state.introduction = introduction
  },
  SET_NAME: (state, name) => {
    state.name = name
  },
  SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
  },
  SET_ROLES: (state, roles) => {
    state.roles = roles
  },
  SET_MENUS: (state, menus) => { //这里是新增的
    state.menus = menus
  }
}

const actions = {
    getMenus({ commit, state }) { //这个是新增的action
    return new Promise((resolve, reject) => {
      getMenus(state.token).then(response => {  //这里的getMenus是调用request方法从服务端获得路由菜单数据的Promise,类似getInfo
        const { data } = response
        console.log(data)

        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const menus = data

        // roles must be a non-empty array
        if (!menus || menus.length <= 0) {
          reject('getMenus: menus must be a non-null array!')
        }

        commit('SET_MENUS', menus)
        resolve(menus)
      }).catch(error => {
        reject(error)
      })
    })
  },
}

还有一个文件是 src/permission.js
要在 store.dispatch('permission/generateRoutes') 代码附近要改一下,原先从本地配置+roles获得用户路由,现在改从服务端获得之后再 addRoutes

          const menus = await store.dispatch('user/getMenus')
          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', { menus })
          // dynamically add accessible routes
          router.addRoutes(accessRoutes)

亲测可行……

用这种方法你的没有提示TypeError: Cannot read property 'range' of null?

关于动态显示菜单问题,困扰我两天,之前设想方案是如何改路由,事实上,大多数情况下只要控制菜单显示即可。这样做的好处是:不需要解决component问题。下面是我的解决方案:
1、后端返回数据(和前端router设置字段匹配):

[
        {
            "id": 127,
            "path": "/system",
            "name": null,
            "hidden": 0,
            "redirect": null,
            "meta": {
                "title": "System",
                "icon": "example"
            },
            "children": [
                {
                    "id": 139,
                    "path": "resources-route",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "ResourcesRoute",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 138,
                    "path": "resources",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "Resources",
                        "icon": "lock"
                    },
                    "children": []
                },
                {
                    "id": 129,
                    "path": "role",
                    "name": null,
                    "hidden": 1,
                    "redirect": null,
                    "meta": {
                        "title": "Role",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 128,
                    "path": "user",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "User",
                        "icon": "clipboard"
                    },
                    "children": []
                }
            ]
        }
    ]

2、修改菜单加载:
文件:src/layout/components/Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
<!--        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />-->
        <sidebar-item v-for="route in menuList" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
import {resourcesForElementAdminTree} from '@/api/resources'

export default {
  components: { SidebarItem, Logo },

  data(){
    return{
      menuList:{}
    }
  },

  created() {
    this.getRoute()
  },
  methods:{
    getRoute(){
      resourcesForElementAdminTree().then(response=>{
        this.menuList=response.data
      })
    }
  },

  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),

    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

大功告成

@zhongshuaiyou
"babel-eslint": "8.2.6" 指定这个版本安装

huxc commented

有没有遇到 进入列表页后,在列表页动态添加新增、详情页路由后 导致刷新进入登陆页面的?

我说下我现在使用的方案,动态路由还是前端自己维护,用户的角色以及每个角色的权限都可以在后台动态设置(可细化到按钮),每个路由和按钮都对应有一个标识,设置的时候后端也是保存此标识,前端用户登录后,后端负责根据用户分配角色把这些标识计算出来返给前端(我这里采用的是一维数组,方便遍历),前端拿到标识可以存储在本地,在根据这个标识去前端维护的路由表中洗出有权限的路由(前端每个路由也有对应的标识),这样可以降低耦合性,前后端唯一关联的就是这个标识,路由path和components都是自己维护。

[
  {
    path: '/website/sites', 
    name: 'get:website/sites', // 标识
    component: () => import('@/views/website/sites/Index')
  },
  {
    path: '/website/sites/add',
    name: 'post:website/sites',
    component: () => import('@/views/website/sites/Edit')
  },
  {
    path: '/website/sites/edit/:id',
    name: 'put:website/sites',
    component: () => import('@/views/website/sites/Edit')
  }
]

这里我使用name来作为标识,不过建议大家不这样做,因为name在vue-router有其他用途,可以换成其他名字,然后标识的格式可以自己来定。
我们项目考虑的是路由都对应了相应的接口,所以这里以api来定义的。
另外,后端返回的标识中还包含了按钮的标识,所以可通过指令来控制是否显示:

 <Button type="primary" icon="md-add" @click="add" v-access="'post:column'">添加</Button>

用接口来定义标识还有个好处就是在我们前端请求对应接口的时候可以吧这个标识在header回传给后端,后端可以验证这个标识用户是否有权限,如果没有说明用户是不具备这个权限的,可以丢弃掉操作。这样不仅可以做到路由鉴权,也可做到接口鉴权。大家可以根据项目来决定使用说明格式。

此方案适合每个角色的权限也要动态修改的情况,也算是比较灵活。如有不足,欢迎大佬们指正

    if (route.parentId === -1) { //如果没有父级菜单
      tmp.component = Layout;
    } else if (route.parentId > -1) {
      tmp.component = (resolve) => require([`@/${route.component}`], resolve)
      // tmp.component = () => import(`@/${route.component}.vue`)
    };

动态拼接组件 只能用第一种方式 第二种一直报错
并且 ,第一种拼接 本地没问题 打包线上后直接报错,有人遇到吗?
Error: Cannot find module './views/supplierManagement/smsSupplierManagement.vue 😥

@shen-lan 同样遇到了这个问题。

由后台配置动态路由的componet,前端路由懒加载的打包不会有问题吗???我一直是前端控制路由,后台控制映射对应权限的路由。即path对应的compent都是前端控制的。显示的菜单则由后台动态生成。addRoute是用在ssr中,那前后端分离的addRoutes在打包中会如何处理?

@shen-lan 同样遇到了这个问题。

@noprom 我这边已经可以了 。是我写错了的原因

关于动态显示菜单问题,困扰我两天,之前设想方案是如何改路由,事实上,大多数情况下只要控制菜单显示即可。这样做的好处是:不需要解决component问题。下面是我的解决方案:
1、后端返回数据(和前端router设置字段匹配):

[
        {
            "id": 127,
            "path": "/system",
            "name": null,
            "hidden": 0,
            "redirect": null,
            "meta": {
                "title": "System",
                "icon": "example"
            },
            "children": [
                {
                    "id": 139,
                    "path": "resources-route",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "ResourcesRoute",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 138,
                    "path": "resources",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "Resources",
                        "icon": "lock"
                    },
                    "children": []
                },
                {
                    "id": 129,
                    "path": "role",
                    "name": null,
                    "hidden": 1,
                    "redirect": null,
                    "meta": {
                        "title": "Role",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 128,
                    "path": "user",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "User",
                        "icon": "clipboard"
                    },
                    "children": []
                }
            ]
        }
    ]

2、修改菜单加载:
文件:src/layout/components/Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
<!--        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />-->
        <sidebar-item v-for="route in menuList" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
import {resourcesForElementAdminTree} from '@/api/resources'

export default {
  components: { SidebarItem, Logo },

  data(){
    return{
      menuList:{}
    }
  },

  created() {
    this.getRoute()
  },
  methods:{
    getRoute(){
      resourcesForElementAdminTree().then(response=>{
        this.menuList=response.data
      })
    }
  },

  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),

    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

大功告成

老哥,后来 有没有遇到啥bug呢, 我遇到点击面包屑导航 控制台报错
vue.runtime.esm.js?2b0e:1888 RangeError: Maximum call stack size exceeded 错误

    if (route.parentId === -1) { //如果没有父级菜单
      tmp.component = Layout;
    } else if (route.parentId > -1) {
      tmp.component = (resolve) => require([`@/${route.component}`], resolve)
      // tmp.component = () => import(`@/${route.component}.vue`)
    };

动态拼接组件 只能用第一种方式 第二种一直报错
并且 ,第一种拼接 本地没问题 打包线上后直接报错,有人遇到吗?
Error: Cannot find module './views/supplierManagement/smsSupplierManagement.vue 😥

我也是怎么弄

我想说,目前处理权限处理阶段,真他么要崩溃了,记录一下2020.8.13,发发牢*!!!

我想把菜单全部由后台查询出来,前端不用配置,请问这个要怎么弄呢?取出来了,也显示了,但是就是不能折叠,点击也是空白页

各位大哥们,如果一开始按照 @/router/index.js 中的 asyncRouterMap 分配角色,将不同的角色对应不同的路由表保存到数据库中。但是一段时间后,源码中的 @/router/index.js 中的 asyncRouterMap 路由表发生变化。那么之前已经被之前增加的角色保存到数据库的路由怎么更新。难道要写个接口把最新的 @/router/index.js 中的 asyncRouterMap 发给后端让后端处理遍历角色表中的路由嘛?有遇到这种情况的嘛?万分感谢!

官方给的文档很不明确,希望能给一个切实可行的demo

我想把菜单全部由后台查询出来,前端不用配置,请问这个要怎么弄呢?取出来了,也显示了,但是就是不能折叠,点击也是空白页

嗯,我也在尝试这个方式,根本也不需要前端去 role 啥的。后端直接生成好对用用户权限的路由就很舒服了。现在遇到 compoent 的时候 can't find module 的问题,

# 组装后端数据成路由的方式

export function generateMenu(routes, menus) {
  menus.forEach(item => {
    const menu = {
      path: item.path,
      component: item.component === 'Layout' ? Layout : () => import(`@${item.component}`),
      hidden: item.hidden,
      children: [],
      name: item.name,
      redirect: item.redirect ? item.redirect : '',
      meta: { title: item.title, icon: item.icon, activeMenu: item.activeMenu, noCache: item.noCache }
    }

    if (item.children && item.children.length > 0) {
      generateMenu(menu.children, item.children)
    } else {
      delete menu.children
    }
    routes.push(menu)
  })
}

# actions
const actions = {
  generateRoutes({ commit }, data) {
    const { roles, menus } = data
    return new Promise(resolve => {
      let accessedRoutes = []
      generateMenu(accessedRoutes, menus)
      accessedRoutes.push({ path: '*', redirect: '/404', hidden: true });
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

可是,坑呀,遇到 chunk-libs.102e56c5.js:46 Error: Cannot find module '@/views/system/permission/index 这样的问题。怎么整呢。

官方给的文档很不明确,希望能给一个切实可行的demo

文档还是不错的,只是没有那么周全,把所有的都呵护到。

是否可以换一个思路,前端路由表在控制权限的时候,是通过meta{roles:[]}这样来实现的,然后后端只需要存储一个能和前端路由表映射的数据,然后前端在登录之后,本身用户已经获取角色信息了,后端不用根据用户角色身份来获取路由表,而是直接将所有的权限查出,然后映射给前端路由,当然这样就会变成纯粹的前端控制路由,但是后端已经存储了路由角色的权限关系,如果需要后端验证,再动态向后端验证就可以了,这样做,只要用户登录,不是根据用户去查路由表,而是默认去查路由表的权限的角色,然后赋值给前端,

@EUEHBin 我上面说的就是你的意思,但是我觉得你有个地方会报错

`function makePermissionRouters(serverRouter, clientRoutes)
{

 for(let k in clientRoutes)
 {
      for(let i in serverRouter)
      {
        if(clientRoutes[k].path === serverRouter[i].path)
        {
          clientRoutes[k].id = serverRouter[i].id
          clientRoutes[k].roles = serverRouter[i].roles

          if(clientRoutes[k].children)
          {
             makePermissionRouters(serverRouter[i].children,clientRoutes[k].children)
          }
        }
      }
 }

}这里有递归调用,const actions = {
async generateRoutes({ commit }, roles)
{
let PermissionRouters = await getRoute().then(res =>
{
const serverRouter = res.data

      //console.log(data);
      makePermissionRouters(serverRouter, clientRoutes)

      PermissionRouters = clientRoutes;

      return PermissionRouters
  })
  //console.log(PermissionRouters)

  return new Promise(resolve =>
  {
    let accessedRoutes

    if (roles.includes('admin'))
    {
      accessedRoutes = PermissionRouters || []
    }
    else
    {
      accessedRoutes = filterAsyncRoutes(PermissionRouters, roles)
    }
    commit('SET_ROUTES', accessedRoutes)
    resolve(accessedRoutes)
  })

}
}`
还有这里你处理合并完之后并没有替换,这样还是原来的逻辑没有变化啊,替换之后就生效没问题了.

这里我用的标识是path,因为有的我没有写name这个参数,但是path是必须的,
现在来说动态权限仅限页面级的很简单,就是前段一份路由表,后端对应标识和roles角色存下就可以了,然后后端取出来和前端合并对应

后端根据登录的用户返回该用户可访问的页面路由name,前端根据此数据过滤路由。

按钮权限的话,后端返回该用户可操作的按钮action,前端在某权限A按钮上判断这个A是否在action列表中,如果有就说明有权限操作该按钮。

这样就不需要考虑角色问题,角色可访问页面和可操作按钮都是可以动态配置的。

不知这样实现可行吗?

再做一个asyncRouterMap.components的name 和 本地components 做一个映射

const map={
 login:require('login/index').default // 同步的方式
 login:()=>import('login/index')      // 异步的方式
}
//你存在服务端的map类似于
const serviceMap=[
 { path: '/login', component: 'login', hidden: true }
]
//之后遍历这个map,动态生成asyncRouterMap
并将 component 替换为map[component]

映射这样写
后台返回的数据结构 res.data.menus
{
path: "/user",
component: 'Layout',
alwaysShow: true,
name: 'UserModule',
meta: {title: "用户管理", icon: "el-icon-s-tools"},
children: [
{ path: "user",
component: "user/user/index",
name: "UserModuleUser",
meta: {title: "用户列表"}
},

{ path: "role", 
  component: "user/role/index", 
  name: "UserModuleRole",
  meta: {title: "角色列表"}
},

{ path: "center", 
  component: "user/center/index", 
  name: "UserModuleCenter",
  hidden: true,
  meta: {title: "个人中心"}
}

]
}

src/store/permission.js
const actions = {
generateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getInfo().then(res => {
const accessedRoutes = filterAsyncRouter(res.data.menus)
accessedRoutes.push({path: '*', redirect: '/404', hidden: true})
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
})
}
}

// 遍历后台传来的路由字符串,转换为组件对象
export function filterAsyncRouter(asyncRouterMap) {
return asyncRouterMap.filter(route => {
if (route.component) {
// Layout组件特殊处理
if (route.component === 'Layout') {
route.component = Layout
} else {
route.component = loadView(route.component)
}
}
if (route.children != null && route.children.length != 0 && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
}

const loadView = (view) => require(@/views/${view}).default

我这边的思路是,前台代码保留一份完整的路由信息表,服务器返回带有name和roles的对应数据即可,前端负责将数据合并,然后加载“动态路由”。
亲测没有问题!
上代码

后台返回数据 mock\role\routes.js

export const asyncRoutes = [
  {
    path: '/managener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'funManagener',
    name: 'FunManagener',
    meta: {
      title: '回收系统功能管理',
      icon: 'tree',
      roles: ['admin']
    }
  },

  {
    path: 'acManagener',
    name: 'AcManagener',
    meta: {
      title: '回收系统账套管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenModule',
    name: 'ScreenModule',
    meta: {
      title: '可视化大屏模板管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'screenAccount',
    name: 'ScreenAccount',
    meta: {
      title: '可视化大屏账号管理',
      icon: 'table',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentnewPlate',
    name: 'AcMangentnewPlate',
    meta: {
      title: ' 新增平台',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentdetail/:id',
    name: 'AcMangentdetail',
    meta: {
      title: ' 详情',
      roles: ['admin']
    }
  },

  {
    path: 'acMangentedit/:id',
    name: 'AcMangentedit',
    meta: {
      title: ' 编辑',
      roles: ['admin', 'editor']
    }
  },

  {
    path: 'acMangentepeizhi/:id',
    name: 'AcMangentepeizhi',
    meta: {
      title: ' 配置功能 ',
      roles: ['admin']
    }
  },
  {
    path: '/system',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: ['admin']
    }
  },

  {
    path: 'rolesManagener',
    name: 'RolesManagener',
    meta: { title: '用户权限管理', icon: 'tree', roles: ['admin'] }
  },

  {
    path: 'userManagener',
    name: 'UserManagener',
    meta: { title: '用户管理', icon: 'table', roles: ['admin'] }
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台数据 src\router\index.js

export const asyncRoutes = [
  {
    path: '/managener',
    component: Layout,
    redirect: '/managener/acManagener',
    name: 'Managener',
    meta: {
      title: '各平台管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'funManagener',
        name: 'FunManagener',
        component: () =>
                 import('@/views/managener/funManagener/index'),
        meta: { title: '回收系统功能管理', icon: 'tree' }
      },
      {
        path: 'acManagener',
        name: 'AcManagener',
        component: () =>
                 import('@/views/managener/acManagener/index'),
        meta: { title: '回收系统账套管理', icon: 'table' }
      },
      {
        path: 'screenModule',
        name: 'ScreenModule',
        component: () =>
                 import('@/views/managener/screenModule/index'),
        meta: { title: '可视化大屏模板管理', icon: 'table' }
      },
      {
        path: 'screenAccount',
        name: 'ScreenAccount',
        component: () =>
                 import('@/views/managener/screenAccount/index'),
        meta: { title: '可视化大屏账号管理', icon: 'table' }
      },
      {
        path: 'acMangentnewPlate',
        name: 'AcMangentnewPlate',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/newPlate/index'),
        meta: {
          title: ' 新增平台',
          icon: 'Rest',
          roles: []
        }
      },
      {
        path: 'acMangentdetail/:id',
        name: 'AcMangentdetail',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/acdetail/index'),
        meta: {
          title: ' 详情',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentedit/:id',
        name: 'AcMangentedit',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/editplate/index'),
        meta: {
          title: ' 编辑',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      },
      {
        path: 'acMangentepeizhi/:id',
        name: 'AcMangentepeizhi',
        hidden: true,
        component: () =>
                 import('@/views/managener/acManagener/peizhi/index'),
        meta: {
          title: ' 配置功能 ',
          icon: 'Rest',
          roles: [],
          noCache: true,
          activeMenu: '/managener/acManagener'
        }
      }
    ]
  },
  {
    path: '/system',
    component: Layout,
    redirect: '/system/rolesManagener',
    name: 'System',
    meta: {
      title: '系统管理',
      icon: 'example',
      roles: []
    },
    children: [
      {
        path: 'rolesManagener',
        name: 'RolesManagener',
        component: () =>
                 import('@/views/system/rolesManagener/index'),
        meta: { title: '用户权限管理', icon: 'tree' }
      },
      {
        path: 'userManagener',
        name: 'UserManagener',
        component: () =>
                 import('@/views/system/userManagener/index'),
        meta: { title: '用户管理', icon: 'table' }
      }
    ]
  },

  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404', hidden: true }
]

前台处理代码 src\store\modules\permission.js

import { asyncRoutes, constantRoutes } from '@/router'
import { getAsyncRoutes } from '../../api/role'
import { deepClone } from '../../utils/index'

const clientRoutes = deepClone(asyncRoutes)
/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 *
 * @param {arr} clientAsyncRoutes 前端保存动态路由
 * @param {arr} serverRouter 后端保存动态路由
 */
function makePermissionRouters(serverRouter, clientAsyncRoutes) {
  clientAsyncRoutes.map(ele => {
    if (!ele.name || (!ele.meta && !ele.meta.roles)) return
    let roles_obj
    for (let i = 0; i < serverRouter.length; i++) {
      const element = serverRouter[i]
      if (ele.name === element.name) {
        roles_obj = element
      }
    }
    ele.meta.roles = roles_obj.meta.roles

    if (ele.children) {
      makePermissionRouters(serverRouter, ele.children)
    }
  })
  return clientAsyncRoutes
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
}

const actions = {
  async generateRoutes({ commit }, roles) {
    let PermissionRouters = await getAsyncRoutes().then(res => {
      const data = res.data
      PermissionRouters = makePermissionRouters(data, clientRoutes)
      // console.log('api:' + PermissionRouters)
      return PermissionRouters
    })
    console.log(PermissionRouters)
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        console.log(PermissionRouters)

        accessedRoutes = PermissionRouters || []
      } else {
        accessedRoutes = filterAsyncRoutes(PermissionRouters, roles)
      }
      console.log(accessedRoutes)

      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

动态路由成功了,但是在页面内刷新会404 是什么原因

image
我是把这个隐藏掉就行了 然后加一个新的404界面的路由就行了。

    if (route.parentId === -1) { //如果没有父级菜单
      tmp.component = Layout;
    } else if (route.parentId > -1) {
      tmp.component = (resolve) => require([`@/${route.component}`], resolve)
      // tmp.component = () => import(`@/${route.component}.vue`)
    };

动态拼接组件 只能用第一种方式 第二种一直报错
并且 ,第一种拼接 本地没问题 打包线上后直接报错,有人遇到吗?
Error: Cannot find module './views/supplierManagement/smsSupplierManagement.vue 😥

我也是遇到这种问题

@hanhaiyuntao 检查你的路径 大大概率是 写错了 。

@/${route.component}.vue` 注意@符号后面的分隔符

我说下我现在使用的方案,动态路由还是前端自己维护,用户的角色以及每个角色的权限都可以在后台动态设置(可细化到按钮),每个路由和按钮都对应有一个标识,设置的时候后端也是保存此标识,前端用户登录后,后端负责根据用户分配角色把这些标识计算出来返给前端(我这里采用的是一维数组,方便遍历),前端拿到标识可以存储在本地,在根据这个标识去前端维护的路由表中洗出有权限的路由(前端每个路由也有对应的标识),这样可以降低耦合性,前后端唯一关联的就是这个标识,路由path和components都是自己维护。

[
  {
    path: '/website/sites', 
    name: 'get:website/sites', // 标识
    component: () => import('@/views/website/sites/Index')
  },
  {
    path: '/website/sites/add',
    name: 'post:website/sites',
    component: () => import('@/views/website/sites/Edit')
  },
  {
    path: '/website/sites/edit/:id',
    name: 'put:website/sites',
    component: () => import('@/views/website/sites/Edit')
  }
]

这里我使用name来作为标识,不过建议大家不这样做,因为name在vue-router有其他用途,可以换成其他名字,然后标识的格式可以自己来定。
我们项目考虑的是路由都对应了相应的接口,所以这里以api来定义的。
另外,后端返回的标识中还包含了按钮的标识,所以可通过指令来控制是否显示:

 <Button type="primary" icon="md-add" @click="add" v-access="'post:column'">添加</Button>

用接口来定义标识还有个好处就是在我们前端请求对应接口的时候可以吧这个标识在header回传给后端,后端可以验证这个标识用户是否有权限,如果没有说明用户是不具备这个权限的,可以丢弃掉操作。这样不仅可以做到路由鉴权,也可做到接口鉴权。大家可以根据项目来决定使用说明格式。

此方案适合每个角色的权限也要动态修改的情况,也算是比较灵活。如有不足,欢迎大佬们指正

现在基于这套思路封装了插件,可以直接使用,https://github.com/BWrong/auth-tool

我说下我现在使用的方案,动态路由还是前端自己维护,用户的角色以及每个角色的权限都可以在后台动态设置(可细化到按钮),每个路由和按钮都对应有一个标识,设置的时候后端也是保存此标识,前端用户登录后,后端负责根据用户分配角色把这些标识计算出来返给前端(我这里采用的是一维数组,方便遍历),前端拿到标识可以存储在本地,在根据这个标识去前端维护的路由表中洗出有权限的路由(前端每个路由也有对应的标识),这样可以降低耦合性,前后端唯一关联的就是这个标识,路由path和components都是自己维护。

[
  {
    path: '/website/sites', 
    name: 'get:website/sites', // 标识
    component: () => import('@/views/website/sites/Index')
  },
  {
    path: '/website/sites/add',
    name: 'post:website/sites',
    component: () => import('@/views/website/sites/Edit')
  },
  {
    path: '/website/sites/edit/:id',
    name: 'put:website/sites',
    component: () => import('@/views/website/sites/Edit')
  }
]

这里我使用name来作为标识,不过建议大家不这样做,因为name在vue-router有其他用途,可以换成其他名字,然后标识的格式可以自己来定。
我们项目考虑的是路由都对应了相应的接口,所以这里以api来定义的。
另外,后端返回的标识中还包含了按钮的标识,所以可通过指令来控制是否显示:

 <Button type="primary" icon="md-add" @click="add" v-access="'post:column'">添加</Button>

用接口来定义标识还有个好处就是在我们前端请求对应接口的时候可以吧这个标识在header回传给后端,后端可以验证这个标识用户是否有权限,如果没有说明用户是不具备这个权限的,可以丢弃掉操作。这样不仅可以做到路由鉴权,也可做到接口鉴权。大家可以根据项目来决定使用说明格式。
此方案适合每个角色的权限也要动态修改的情况,也算是比较灵活。如有不足,欢迎大佬们指正

现在基于这套思路封装了插件,可以直接使用,https://github.com/BWrong/auth-tool

你这样做的话还是不够灵活 没办法直接去动态设置路由的标题 图标啥的 ,有什么修改需要重新编译打包才行

我说下我现在使用的方案,动态路由还是前端自己维护,用户的角色以及每个角色的权限都可以在后台动态设置(可细化到按钮),每个路由和按钮都对应有一个标识,设置的时候后端也是保存此标识,前端用户登录后,后端负责根据用户分配角色把这些标识计算出来返给前端(我这里采用的是一维数组,方便遍历),前端拿到标识可以存储在本地,在根据这个标识去前端维护的路由表中洗出有权限的路由(前端每个路由也有对应的标识),这样可以降低耦合性,前后端唯一关联的就是这个标识,路由path和components都是自己维护。

[
  {
    path: '/website/sites', 
    name: 'get:website/sites', // 标识
    component: () => import('@/views/website/sites/Index')
  },
  {
    path: '/website/sites/add',
    name: 'post:website/sites',
    component: () => import('@/views/website/sites/Edit')
  },
  {
    path: '/website/sites/edit/:id',
    name: 'put:website/sites',
    component: () => import('@/views/website/sites/Edit')
  }
]

这里我使用name来作为标识,不过建议大家不这样做,因为name在vue-router有其他用途,可以换成其他名字,然后标识的格式可以自己来定。
我们项目考虑的是路由都对应了相应的接口,所以这里以api来定义的。
另外,后端返回的标识中还包含了按钮的标识,所以可通过指令来控制是否显示:

 <Button type="primary" icon="md-add" @click="add" v-access="'post:column'">添加</Button>

用接口来定义标识还有个好处就是在我们前端请求对应接口的时候可以吧这个标识在header回传给后端,后端可以验证这个标识用户是否有权限,如果没有说明用户是不具备这个权限的,可以丢弃掉操作。这样不仅可以做到路由鉴权,也可做到接口鉴权。大家可以根据项目来决定使用说明格式。
此方案适合每个角色的权限也要动态修改的情况,也算是比较灵活。如有不足,欢迎大佬们指正

现在基于这套思路封装了插件,可以直接使用,https://github.com/BWrong/auth-tool

你这样做的话还是不够灵活 没办法直接去动态设置路由的标题 图标啥的 ,有什么修改需要重新编译打包才行

图标是在后台数据返回的,修改图标直接去后台修改就行了,同理需要在管理端控制的都可以放到后台去维护.如菜单名字、图标等等,因为除了permission这个字段,其他是完全解耦的,你想在后台控制什么就取决于你的数据结构
image
这个是后台返回的菜单数据,是带上图标的,前端根据这个来渲染图标。
image

看了下 你这种方式和另外一种方式直接返回对应角色路由表区别并不大
#293 (comment)
唯一的区别是把路由的path和component交给了本地配置,感觉必要性并不高@BWrong

看了下 你这种方式和另外一种方式直接返回对应角色路由表区别并不大
#293 (comment)
唯一的区别是把路由的path和component交给了本地配置,感觉必要性并不高@BWrong

这个看自己吧,我这样做主要是最大程度和后台解耦,这样path和component就完全由前端自己控制,而不是我要调整一下组件位置或者路由还需要去后台修改一下,而且也可以减少一些配置项,不用在后台暴露前端路由组件的文件结构

关于动态显示菜单问题,困扰我两天,之前设想方案是如何改路由,事实上,大多数情况下只要控制菜单显示即可。这样做的好处是:不需要解决component问题。下面是我的解决方案:
1、后端返回数据(和前端router设置字段匹配):

[
        {
            "id": 127,
            "path": "/system",
            "name": null,
            "hidden": 0,
            "redirect": null,
            "meta": {
                "title": "System",
                "icon": "example"
            },
            "children": [
                {
                    "id": 139,
                    "path": "resources-route",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "ResourcesRoute",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 138,
                    "path": "resources",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "Resources",
                        "icon": "lock"
                    },
                    "children": []
                },
                {
                    "id": 129,
                    "path": "role",
                    "name": null,
                    "hidden": 1,
                    "redirect": null,
                    "meta": {
                        "title": "Role",
                        "icon": "clipboard"
                    },
                    "children": []
                },
                {
                    "id": 128,
                    "path": "user",
                    "name": null,
                    "hidden": 0,
                    "redirect": null,
                    "meta": {
                        "title": "User",
                        "icon": "clipboard"
                    },
                    "children": []
                }
            ]
        }
    ]

2、修改菜单加载:
文件:src/layout/components/Sidebar/index.vue

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
<!--        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />-->
        <sidebar-item v-for="route in menuList" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
import {resourcesForElementAdminTree} from '@/api/resources'

export default {
  components: { SidebarItem, Logo },

  data(){
    return{
      menuList:{}
    }
  },

  created() {
    this.getRoute()
  },
  methods:{
    getRoute(){
      resourcesForElementAdminTree().then(response=>{
        this.menuList=response.data
      })
    }
  },

  computed: {
    ...mapGetters([
      'permission_routes',
      'sidebar'
    ]),

    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

大功告成

我觉得这个思路是最棒的

component: () => import('@/views/${component}') + babel-plugin-dynamic-import-webpack 可以实现动态加载菜单,但是不能热更新了开发模式下

@wcooltime 你能详细说明下吗?,动态加载菜单是没问题,关键是我想用后端实现,而现在我打算用node来处理前端文件

使用babel-eslint(官方废弃了此包)会导致下面这种import动态导入写法报错

export const loadView = (view) => {
return () => import(@/views/${view}.vue)
}

换成@babel/eslint-parser就可以了。

思路基本差不多,根据后端返回的权限树,动态匹配出一份路由 再 addRoutes

通过递归后台传过来的数据 转成router
function format1(routerList) {
let children = [];
let childRouter = {};
routerList.forEach(child => {
if(child.userRouters){ //菜单下如果还有子目录 则递归调用
childRouter = {
path: child.rPath,
name: child.rTitle,
component: resolve => require(['@/views' + child.rComponent], resolve),
meta: {title: child.rTitle, icon: child.rIcon},
alwaysShow:true,
children:format1(child.userRouters)
}
}else{
childRouter = {
path: child.rPath,
name: child.rTitle,
component: resolve => require(['@/views' + child.rComponent], resolve),
meta: {title: child.rTitle, icon: child.rIcon}
}
}
children.push(childRouter);
})
return children;
}

hudt commented

前端的路由应该被描述为资源(或者叫权限点),而不是和role耦合在一起。

具体做法是把路由表中的roles(哪些角色能访问)改为permission(这个路由的资源名称)。后台负责维护登录人具有哪些角色,角色具有哪些permission,前端通过接口获取当前登录人的permissionList,与路由表permission匹配出正确的动态路由表再addRoutes。

同理,按钮级的permission也应该被描述为资源,而不是和role耦合在一起。

我的解决方案
查询后端路由表,前端再根据返回记录组装成路由需要的格式addRoutes