项目要求给每个角色分配不同的菜单和按钮,Router中的addRoute和beforeEach可以完美实现这个功能。
动态路由
- 可以新建一个route-mock.js的文件,先去把静态路由改造一下。
改造前:
{
path: '/admin/customer',
name: 'customer',
redirect: '/admin/customer/list',
meta: { hidden: false, title: '客户', isHeadNav; true },
component: adminLayout,
children: [
{
path: '',
name: 'safety',
meta: { hidden: false, title: '客户安全', icon: 'pie-chart' },
component: routeLayout,
redirect: '/admin/customer/safety-center',
children: [
{
path: '/admin/customer/safety-center',
name: 'safetyCenter',
meta: { hidden: false, title: '客户安全中心', icon: 'pie-chart' },
component: () => import('@/views/admin/customer-manage/safety/index.vue')
},
]
},
}
改造后:
{
path: '/admin/customer',
name: 'customer',
redirect: '/admin/customer/list',
meta: { hidden: false, title: '客户', isHeadNav: true },
component: 'adminLayout',
children: [
{
path: '',
name: 'safety',
meta: { hidden: false, title: '客户安全', icon: 'pie-chart' },
component: 'routeLayout',
redirect: '/admin/customer/safety-center',
children: [
{
path: '/admin/customer/safety-center',
name: 'safetyCenter',
meta: { hidden: false, title: '客户安全中心', icon: 'pie-chart' },
component: '/admin/customer-manage/safety'
},
]
},
}
只把component字段改成文件路径就好了 如果是Layout布局就直接变成字符串。然后以此数据结构要求后端同事返回。如果需要控制按钮的显隐 则需要在meta里面新增一个自定义字段,比如 { btn: [] },里面存放页面需要的按钮权限。
- 新建store>permission.js 文件 用来存放和处理动态路由。 获取到后台返回路由数据之后因为component字段的原因 我们是无法直接使用的 所以在getUserMenu方法里面去动态的引入组件和处理一些其他逻辑,比如菜单的显隐:
import routeMock from '@/route-mock'
getUserMenu({commit}, payload) {
// payload: tokan
return new Promise(async (resolve, reject) => {
// to do axios...
// 从接口中获取菜单数据 现在用router-mock.js 中的假数据代替
if(routeMock){
let navMenu = []
let btnPermissions = []
routeMock.forEach(v => {
const { meta = {}, children, name, path, redirect, component } = v
// 获取顶部导航
isHeadNav && navMenu.push({meta, name, path, redirect, component, title: meta.title})
// 递归过滤掉不显示的和获取按钮权限
const { res, permissions } = filterAsyncRoutes(children)
v.children = res
// state 中存放按钮权限的对象
// 格式为 `{菜单管理: ['add', 'edit']}`
btnPermissions.push(permissions)
})
store.commit('permission/set_btn_permissions', btnPermissions)
store.commit('permission/set_nav_menu', navMenu)
resolve(routeMock)
}
else {
reject("Navigation menu list acquisition failed!")
}
})
},
let permissions = {}
/**
* 路由过滤器 & 获取按钮权限
* @param {array} routes - router中children的值
* */
function filterAsyncRoutes(routes) {
let res = []
routes && routes.forEach(v => {
if(v.children && v.children.length) {
const { res } = filterAsyncRoutes(v.children)
v.children = res
}
if( Object.keys(v.meta).length){
// 过滤掉hidden
res = routes.filter(v => !v.meta.hidden)
// 只要children有值就可以一直循环取meta的值
if( Array.isArray(v.meta.btn) && v.meta.btn.length) {
permissions[v.meta.title] = v.meta.btn
}
}
})
return { res, permissions}
}
到这一步 我们就得到了能直接用的 routes,接下来就是用 addRoute 和 beforeEach 去放到Router里面。
- 用
beforeEach去监听 接下来要跳转的跳转去向,然后把刚刚处理好的routes放进去, 但是呢 这个地方是最坑的 因为稍微不注意就会进入死循环 所以自己的逻辑一定要十分的清晰 总体分为两大部分,一是用户已经登陆,二是未登录。
未登录还好,就两个跳转选择: 登录页和白名单页面。
我们接下来重点说一下已经登录的 这也是动态路由的核心。 首先如果用户再次访问首页或者登录页 都让路由重定向到项目首页, 如果用户是正常跳转页面那我们就去判断一下在store中有没有我们已经处理好的路由。 如果有就直接 next(), 如果没有我们就要去从后台接口中去取,然后就回到第一步。 拿到数据之后我们就要用 addRoute 去add到Router中去。
const whiteList = ['/user/login']
router.beforeEach(async (to, from, next) => {
const token = Vue.ls.get(AccessToken)
if(token){
if(to.path === '/user/login' || to.path === '/'){
next('/admin/control/index') // 项目首页
} else {
const { menu } = store.state.permission
if(Array.isArray(menu) && menu.length > 0){
next()
} else {
try {
const route = await store.dispatch('permission/getUserMenu', token)
let accessRoutes = await store.dispatch('permission/generateRoutes', route)
accessRoutes.forEach( item => router.addRoute(item))
router.options.routes = router.options.routes.concat(accessRoutes)
next({...to, replace: true})
}
catch (e) {
console.error('动态路由报错:', e)
next({ path: "/user/login", query: { redirect: to.fullPath } })
} finally {
NProgress.done()
}
}
}
} else {
// 白名单免登陆配置
if(whiteList.indexOf(to.path) !== -1){
next()
} else {
next('/user/login')
}
NProgress.done()
}
})
至于这一个行代码:router.options.routes = router.options.routes.concat(accessRoutes) 我们在vue组件中获取当前路由, 但是当前路由是动态的所以就会出现获取不到的情况。这行代码就是处理这个问题的。
你以为就完了吗? 还没... 这时候你会发现当你每次刷新页面的时候,动态页面都会进入404页面。那是因为你在静态路由表里面配置了 { path: '*', redirect: '/404', hidden: true } 它执行的要比动态获取的路由要快,所以你每次刷新的时候 Router会把你的动态路由识别成 * 然后直接跳转到 /404 页面
解决方案就是通配符的也让后端同事返回,或者手动添加到路由的末尾。
按钮权限
这个就很简单了, 我们刚刚也在 filterAsyncRoutes 方法中处理好了, 所以直接哪来就可以。 不过再此之前还要做一件事情。 就是自定义指令:
import Vue from 'vue'
import store from '../store'
Vue.directive('has', {
inserted: function (el, binding) {
let isExist = false
let btnPermissions = store.state.permission.permissions[0]
if(Object.keys(btnPermissions).length && binding.value.length === 2){
let key = binding.value[0]
let value = binding.value[1]
if(btnPermissions[key] && btnPermissions[key].includes(value)) {
isExist = true
}
if (el.parentNode && !isExist) {
el.parentNode.removeChild(el)
}
}
}
})
上面已经讲过了 btnPermissions 的格式是 {菜单管理: ['add', 'edit']},所以我们只需要在按钮上添加上我们的自定义指令就好了: v-has="['菜单名称', '按钮标识']"。
没权限的一定要用 removeChild 删除,要不然按钮权限就没了意义。 切记哈~