相信很多的前端工作者都遇到过路由动态权限的需求,有些小伙伴一时之间也不知道该如何下手
本文将带着你一起去实现根据角色权限来控制路由权限
业务需求:
- 客户端角色分为超级管理员,普通管理员,普通用户等不同等级
- 服务端动态配置各等级可访问的前端页面
- 前端根据服务端下发的角色权限来动态渲染路由和菜单(后台管理平台菜单)
从需求看逻辑
很多的小伙伴在工作中拿到一个需求后不知道该如何下手,这是经验不足和想法不周全的一个表现,在上述的需求中前端小伙伴们需要去做哪些事呢?
- 不难看出最重要也是最核心的是前端动态去渲染路由和菜单
- 服务端下发的角色权限,至于下发的数据是什么样的,那必然是服务端来配合前端更轻松的实现了(在我知道的很多实际开发中,不少的前端工作者只是一味的去配合后端开发,那必然会存在很多的问题,因为后端不一定能准确知道你需要什么样的数据,不知道你使用的框架特性,所以一味的附和会导致很多时候数据结构并不是自己想要的)
- 了解自己需要什么样的数据,以便于在实现起来更轻松
需要的数据
接下来我们看看需要什么样的数据,才能更轻松的实现动态路由
首先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"
}
]
}
]
}
到这里,一个根据角色权限动态渲染路由的需求就大体上完成了,当然了这只是给大家提供一个思路,具体的方案实现各公司可能不同,需要自己结合各自的需求实现,如果有想法的话可以留言一起讨论,觉得写的还行的,请不要吝惜你的赞噢,谢谢观看