【Vue动态路由与按钮权限】

378 阅读4分钟

项目要求给每个角色分配不同的菜单和按钮,Router中的addRoutebeforeEach可以完美实现这个功能。

动态路由

  1. 可以新建一个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: [] },里面存放页面需要的按钮权限。

  1. 新建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,接下来就是用 addRoutebeforeEach 去放到Router里面。

  1. 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 删除,要不然按钮权限就没了意义。 切记哈~