Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
reuwi opened this issue Nov 22, 2017 · 96 comments
Closed

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

reuwi opened this issue Nov 22, 2017 · 96 comments
Labels

Comments

@reuwi
Copy link
Contributor

reuwi commented Nov 22, 2017

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

@PanJiaChen
Copy link
Owner

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

@reuwi
Copy link
Contributor Author

reuwi commented Nov 22, 2017

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

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

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

@PanJiaChen
Copy link
Owner

PanJiaChen commented Nov 22, 2017

再做一个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
Copy link
Contributor Author

reuwi commented Nov 22, 2017

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

@reuwi
Copy link
Contributor Author

reuwi commented Jan 18, 2018

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

@reuwi
Copy link
Contributor Author

reuwi commented Jan 18, 2018

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

@PanJiaChen
Copy link
Owner

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

@flyliao
Copy link

flyliao commented Apr 9, 2018

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

@reuwi
Copy link
Contributor Author

reuwi commented Apr 9, 2018

@flyliao #286

@baidan
Copy link

baidan commented Jun 26, 2018

其实和上面处理方式差不多。
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这个必然前端要做修改的。所以我认为放一份全部的菜单在前端是合理的。

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

@baidan
Copy link

baidan commented Jun 26, 2018

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

@PanJiaChen
Copy link
Owner

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

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

@ipengyo
Copy link

ipengyo commented Jul 4, 2018

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

这样做有如下好处

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

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

相当于 与角色解耦了。

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

@heixin8
Copy link

heixin8 commented Sep 11, 2018

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

@HoHo1
Copy link

HoHo1 commented Sep 21, 2018

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

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

@heixin8
Copy link

heixin8 commented Sep 21, 2018

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

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

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

@BoBoooooo
Copy link

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

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

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

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

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

@BoBoooooo
Copy link

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

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

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

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

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

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

@xiapenghui
Copy link

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

@tianling456
Copy link

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

@yinchengnuo
Copy link

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

@wangmingweikong
Copy link

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

@vini123
Copy link

vini123 commented Oct 13, 2020

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

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

@vini123
Copy link

vini123 commented Oct 13, 2020

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

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 这样的问题。怎么整呢。

@vini123
Copy link

vini123 commented Oct 13, 2020

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

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

@youhujun
Copy link

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

@youhujun
Copy link

@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角色存下就可以了,然后后端取出来和前端合并对应

@liu-jia-yi
Copy link

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

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

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

不知这样实现可行吗?

@IreneJinqiu
Copy link

再做一个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

@SuperZiLu
Copy link

我这边的思路是,前台代码保留一份完整的路由信息表,服务器返回带有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界面的路由就行了。

@hanhaiyuntao
Copy link

    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
Copy link

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

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

@BWrong
Copy link

BWrong commented May 14, 2021

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

@ghost
Copy link

ghost commented May 14, 2021

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

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

@BWrong
Copy link

BWrong commented May 14, 2021

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

@ghost
Copy link

ghost commented May 15, 2021

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

@BWrong
Copy link

BWrong commented May 15, 2021

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

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

@rdisme
Copy link

rdisme commented Jun 8, 2021

关于动态显示菜单问题,困扰我两天,之前设想方案是如何改路由,事实上,大多数情况下只要控制菜单显示即可。这样做的好处是:不需要解决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>

大功告成

我觉得这个思路是最棒的

@wcooltime
Copy link

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

@youhujun
Copy link

youhujun commented Jul 8, 2021

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

@rehack
Copy link

rehack commented Aug 10, 2021

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

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

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

@Corgis
Copy link

Corgis commented Aug 12, 2021

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

@retname
Copy link

retname commented Aug 13, 2021

通过递归后台传过来的数据 转成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
Copy link

hudt commented Feb 13, 2023

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

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

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

@tianling456
Copy link

tianling456 commented Feb 13, 2023 via email

@mizuhokaga
Copy link

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

@tianling456
Copy link

tianling456 commented Jul 23, 2023 via email

@daweiyong
Copy link

daweiyong commented Jul 23, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests