vue实现动态权限与菜单

相信很多的前端工作者都遇到过路由动态权限的需求,有些小伙伴一时之间也不知道该如何下手

本文将带着你一起去实现根据角色权限来控制路由权限

业务需求:
  • 客户端角色分为超级管理员,普通管理员,普通用户等不同等级
  • 服务端动态配置各等级可访问的前端页面
  • 前端根据服务端下发的角色权限来动态渲染路由和菜单(后台管理平台菜单)
从需求看逻辑

很多的小伙伴在工作中拿到一个需求后不知道该如何下手,这是经验不足和想法不周全的一个表现,在上述的需求中前端小伙伴们需要去做哪些事呢?

  • 不难看出最重要也是最核心的是前端动态去渲染路由和菜单
  • 服务端下发的角色权限,至于下发的数据是什么样的,那必然是服务端来配合前端更轻松的实现了(在我知道的很多实际开发中,不少的前端工作者只是一味的去配合后端开发,那必然会存在很多的问题,因为后端不一定能准确知道你需要什么样的数据,不知道你使用的框架特性,所以一味的附和会导致很多时候数据结构并不是自己想要的
  • 了解自己需要什么样的数据,以便于在实现起来更轻松
需要的数据

接下来我们看看需要什么样的数据,才能更轻松的实现动态路由
首先vue的路由也就是router,router定义了前端所有的页面路由,这可以看成一个总的前端路由表
在这个路由表里,有一些页面是需要去区分用户权限的,有一些是公共的不需要区分权限,首先说下第一个思路,也是vue-router官方推荐的方式

vue-router官方推荐定义路由的时候可以配置 meta 字段,这样我们在定义路由的时候就增加上每个路由的role信息
meta: { role: ['admin','super_admin'] }表示该页面只有管理员和超级管理员才能有资格进入

{
      path: 'operationRecord',
      name: '操作记录',
      component: () => import('@/views/basic/operationRecord/index.vue'),
      meta: { role: ['admin', 'super_admin'] }
}

当然了很多的时候不一定是这样的,也许下发的是当前角色的权限所能访问的页面集合,而不只是角色的名称,这个时候meta标签不需要去加什么权限role字段,当然了两种方式的实现本质是一致的,都是根据下发的数据去动态匹配本地总的路由表

实现的方式

vue2.2.0以后新增了router.addRoutes,这个api就是我们实现动态路由的钥匙
实现的思路如下

  • 本地存储一份公共的路由表(任意角色都可访问的路由集合)
  • 服务端下发当前角色的权限list,前端通过匹配list得到该角色最终的路由表
  • 用router.addRoutes添加用户可访问的路由表
  • 使用vuex管理用户路由表,动态渲染菜单(后台管理平台菜单)

这里以vue-admin-template项目为例,上代码(重点
router

// router的index.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
export const projectBasicRoutes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/register',
    name: 'register',
    component: () => import('@/views/login/register'),
    hidden: true
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/views/404'),
    hidden: true
  },
  {
    path: '/',
    name: 'dashboard',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: '首页',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'shouye' }
    }]
  }
]

const createRouter = () => new Router({
  mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: projectBasicRoutes
})

const router = createRouter()
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

projectBasicRoutes 就是我们声明的公共的路由表,这里要注意{ path: '*', redirect: '/404', hidden: true }不能声明在projectBasicRoutes 里面,否则动态添加的路由都会被拦截到404

自己定义的global.js来处理vue实例化之前的操作,如获取权限,定位之类的需求都可以写在这里

// 自己定义的global.js来处理vue实例化之前的操作,如获取权限,定位之类的需求都可以写在这里
import { getRoleAccess } from '@/api/user'
class Global {
  constructor(Vue, store) {
    this.$vue = Vue
    this.store = store
  }
  // vue实例前的build
  async build() {
    await this.getRoleJurisdiction()
    return Promise.resolve()
  }
  getRoleJurisdiction() {
    return new Promise((resolve, reject) => {
      getRoleAccess().then(res => {
        if (res.code === 200 && res.data) {
          // 用户权限列表
          this.store.dispatch('user/setAccessList', res.data)
        }
        resolve(res)
      }).catch((err) => {
        resolve(err)
      })
    })
  }
}
export default Global

main.js(引入自己定义的实例化前的global.js)

import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'

import ElementUI from 'element-ui' // ElementUI
import 'element-ui/lib/theme-chalk/index.css' // ElementUI
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import '@/styles/index.scss' // global css
import '@/icons' // icon
import '@/permission' // permission control
import Global from './global'
window.$global = new Global(Vue, store)
Vue.use(ElementUI)

Vue.config.productionTip = false
window.$global.build().then(() => {
  new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
  })
})

permission.js

// permission.js
import router, { projectBasicRoutes, resetRouter } from './router'
import store from './store'
import commodityRouter from '@/router/modules/commodity'
import orderRouter from '@/router/modules/order'
import basicSettings from '@/router/modules/basic'
import storedecorate from '@/router/modules/storedecorate'
import financeRouter from '@/router/modules/finance'
import operationRouter from '@/router/modules/operation'
import capitalRouter from '@/router/modules/capital'
import userRouter from '@/router/modules/user'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
const constantRouterMap = [commodityRouter, orderRouter, basicSettings, storedecorate, financeRouter, operationRouter, capitalRouter, userRouter]

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/register'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      const accessList = store.state.user.accessList || []
      if (accessList.length > 0) {
        const constantRouter = handleRoleAccess()
        const totalRoutes = [...projectBasicRoutes, ...constantRouter, { path: '*', redirect: '/404', hidden: true }]
        resetRouter()
        router.options.routes = totalRoutes
        router.addRoutes(totalRoutes)
        next({ ...to, replace: true })
        NProgress.done()
      } else {
        next()
        NProgress.done()
      }
    }
  } else {
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

function handleRoleAccess() {
  const accessList = store.state.user.accessList || []
  const accessRoutes = []
  constantRouterMap.map((item) => {
    const firstRouter = item
    for (var i = 0; i < accessList.length; i++) {
      if (item.name === accessList[i].accessCode) {
        const childrenArr = []
        const vuexChildren = []
        if (accessList[i].children && accessList[i].children.length > 0) {
          accessList[i].children.map((citem) => {
            vuexChildren.push(citem.accessCode)
          })
        }
        if (item.children && item.children.length > 0) {
          item.children.map((iitem) => {
            if (vuexChildren.indexOf(iitem.name) !== -1) {
              childrenArr.push(iitem)
            }
          })
        }
        firstRouter.children = childrenArr
        accessRoutes.push(firstRouter)
      }
    }
  })
  store.dispatch('user/setAccessList', [])
  return accessRoutes || []
}

在路由守卫里面进行的权限路由的匹配
注意事项

  • 在使用router.addRoutes之前要调用resetRouter来重置本地路由,避免路由重复添加了
  • router.options.routes = totalRoutes 这行代码的作用是重新渲染路由菜单列表,不要忘记写了
  • { path: '*', redirect: '/404', hidden: true }写在totalRoutes 的最后,来拦截没有权限的路由到404
  • next({ ...to, replace: true })来确保addRoutes()时动态添加的路由已经被完全加载上去
  • 一定要判断accessList.length不大于0的情况,避免next({ ...to, replace: true })死循环的情况
  • handleRoleAccess方法是用来处理下发的权限集合和本地总的路由表的匹配方法
  • vuex的作用是存储服务端下发的数据,临时存储起来,然后在router.addRoutes执行之后清空临时数据,渲染最终的页面

在最后放上一份仅供参考的服务端下发数据

{
    "code":200,
    "message":"操作成功",
    "data":[
        {
            "name":"commodity",
            "id":"1",
            "children":[
                {
                    "name":"Creation",
                    "id":"11"
                },
                {
                    "id":"12",
                    "name":"Exactsearch"
                },
                {
                    "name":"syncConfig",
                    "id":"13"
                }
            ]
        },
        {
            "name":"finance",
            "id":"2",
            "children":[
                {
                    "name":"purchase",
                    "id":"21"
                },
                {
                    "name":"withdrawalexamine",
                    "id":"22"
                }
            ]
        }
    ]
}

到这里,一个根据角色权限动态渲染路由的需求就大体上完成了,当然了这只是给大家提供一个思路,具体的方案实现各公司可能不同,需要自己结合各自的需求实现,如果有想法的话可以留言一起讨论,觉得写的还行的,请不要吝惜你的赞噢,谢谢观看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容