zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

vue实现动态权限与菜单

2023-03-20 15:00:03 时间

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

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

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

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

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

接下来我们看看需要什么样的数据,才能更轻松的实现动态路由 首先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"
                }
            ]
        }
    ]
}

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