目录

  • 为什么要使用动态路由?
  • 添加菜单接口 及 菜单状态管理根据得到的菜单生成动态路由根据 vuex 中的暂存的菜单生成侧边菜单栏

为什么要使用动态路由?

一般系统中,都会有不同权限的操作人员,这时候他们看到的页面也将会不同,因此都需要根据他们的权限来生成对应的菜单,这个就得通过动态路由来实现。

主流的两种实现方式

控制一般都是由前端的路由中设置。后端返回路由表动态生成两种。

前端控制

  • 不用后端控制,只用返回角色、
  • 根据可能有的角色,在对应路由上维护相关角色
  • 在登录后,判断路由中维护的角色是否吻合来动态添加生成

后端控制

  • 路由存在数据库中,可动态维护。且相对安全。
  • 登录后,获取动态路由信息。
  • 得到路由后,匹配文件,生成路由,添加

后端控制路由 实现

由于我这里是用 fastmock 模拟的数据,实际中还请自行生成。

mock 数据如下:

{    "code": 200,    "success": true,    "data": [        {            "menuId": 2,            "menuName": "一级菜单",            "parentMenuId": 0,            "url": "menu/singleMenu/index",            "type": 1,            "icon": "el-icon-wind-power",            "orderIndex": 1,            "children": [            ]        },        {            "menuId": 3,            "menuName": "二级菜单",            "parentMenuId": 0,            "url": "",            "type": 1,            "icon": "el-icon-ice-cream-round",            "orderIndex": 1,            "children": [                {                    "menuId": 301,                    "menuName": "二级1-1",                    "parentMenuId": 3,                    "url": "menu/secondMenu/second1-1",                    "type": 2,                    "icon": "el-icon-milk-tea",                    "orderIndex": 1,                    "children": [                    ]                },                {                    "menuId": 302,                    "menuName": "二级1-2",                    "parentMenuId": 3,                    "url": "menu/secondMenu/second1-2",                    "type": 2,                    "icon": "el-icon-potato-strips",                    "orderIndex": 2,                    "children": [                    ]                },                {                    "menuId": 303,                    "menuName": "二级1-3",                    "parentMenuId": 3,                    "url": "menu/secondMenu/second1-3",                    "type": 2,                    "icon": "el-icon-lollipop",                    "orderIndex": 3,                    "children": [                    ]                }            ]        },        {            "menuId": 4,            "menuName": "三级多级菜单",            "parentMenuId": 0,            "url": "",            "type": 1,            "icon": "el-icon-ice-cream-round",            "orderIndex": 1,            "children": [                {                    "menuId": 401,                    "menuName": "三级1-1",                    "parentMenuId": 4,                    "url": "menu/threeMenu/three1-1",                    "type": 2,                    "icon": "el-icon-milk-tea",                    "orderIndex": 1,                    "children": [                    ]                },                {                    "menuId": 402,                    "menuName": "二级1-2",                    "parentMenuId": 4,                    "url": "",                    "type": 2,                    "icon": "el-icon-potato-strips",                    "orderIndex": 2,                    "children": [                        {                            "menuId": 40201,                            "menuName": "三级1-2-1",                            "parentMenuId": 402,                            "url": "menu/threeMenu/nextMenu/three1-2-1",                            "type": 2,                            "icon": "el-icon-milk-tea",                            "orderIndex": 1,                            "children": [                            ]                        },                        {                            "menuId": 40202,                            "menuName": "三级1-2-2",                            "parentMenuId": 402,                            "url": "menu/threeMenu/nextMenu/three1-2-2",                            "type": 2,                            "icon": "el-icon-potato-strips",                            "orderIndex": 2,                            "children": [                            ]                        }                    ]                }            ]        }    ],    "message": "成功"}

添加菜单接口 及 菜单状态管理

由于这里是 mock 的。所以就判断了下登录用户名。代码如下:

export const getMenu = (username) => {    if (username == 'user') {        return axios.Get('api/usermenu')    } else {        return axios.Get('api/menu')    }}

状态管理用于保存当前理由加载状态,和菜单值。

再在 actions 中添加一个获取菜单的方法,完整代码如下:

//引入接口import { getMenu } from '@/api/modules/system'export default {    state: {        menuTree: [],        menuLoad: false, //菜单是否已加载状态避免重复加载,刷新又将变为false。    },    getters: {        menuLoad: (state) => {            return state.menuLoad        },    },    mutations: {        setMenuLoad(state, menuLoad) {            state.menuLoad = menuLoad        },        setMenuTree(state, menuTree) {            state.menuTree = menuTree        },    },    actions: {        getMenuTree({ commit }, username) {            return new Promise((resolve, reject) => {                getMenu(username)                    .then((res) => {                        if (res.code === 200) {                            if (res.success) {                                commit('setMenuTree', res.data)                            } else {                                // TODO 处理错误消息                            }                            resolve(res.data)                        }                    })                    .catch((error) => {                        reject(error)                    })            })        },    },}

根据得到的菜单生成动态路由

在这里由于退出时,会导致路由和加载状态不会更新,也不会重置路由的原因,完整代码中包含相关处理。

import Vue from 'vue'import VueRouter from 'vue-router'import login from '@/views/login'import store from '@/store'import { getMenu } from '@/api/modules/system'Vue.use(VueRouter)const originalPush = VueRouter.prototype.pushVueRouter.prototype.push = function push(location) {    return originalPush.call(this, location).catch((err) => err)}const routes = [    {        path: '/',        name: 'home',        component: () => import('@/layout'),        children: [            {                path: '',                name: 'index',                component: () => import('@/views/home/defaultPage'),                meta: {                    title: '首页',                    index: 0,                },            },        ],    },    {        path: '/login',        name: 'login',        component: login,        meta: {            title: '登录',        },    },    {        path: '/notfound',        name: 'notfound',        component: () => import('@/views/notfound'),        meta: {            title: '未找到',        },    },]const defultRouter = () => {    return new VueRouter({        routes: routes,    })}//每次使用默认路由const router = defultRouter()// 解决addRoute不能删除动态路由问题export function resetRouter() {    const reset = defultRouter()    router.matcher = reset.matcher}const WhiteListRouter = ['/login', '/notfound'] // 路由白名单//导航守卫  路由开始前router.beforeEach(async (to, from, next) => {    let user = store.getters.userInfo    let token = store.getters.token    var hasAuth = user !== null && token !== null && user !== undefined && token !== undefined    if (to.path == '/login') {        if (hasAuth) {            next({ path: '/' })        } else {            next()        }    } else {        if (!hasAuth) {            //没登录的情况下  访问的是否是白名单            if (WhiteListRouter.indexOf(to.path) !== -1) {                next()            } else {                next({                    path: '/login',                    query: {                        redirect: to.fullPath,                    },                })            }        } else {            if (store.state.app.menuLoad) {                // 已经加载过路由了                next()                return            } else {                console.log(user.username)                // 使用 await 进行同步处理                const menu = await store.dispatch('getMenuTree', user.username)                console.log(menu)                // 加载动态菜单和路由                addDynamicMenuRoute(menu)                //next({ ...to, replace: true }); // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record                next()            }        }    }})//刷新 加载完后 加载未找到路由  此方法只会在刷新后加载一次router.onReady(() => {    var notfund = [        {            path: '*',            redirect: '/notfound',            name: 'notfound',            component: () => import('@/views/notfound'),            meta: {                title: '未找到',            },        },    ]    router.options.routes = router.options.routes.concat(notfund)    router.addRoutes(router.options.routes)})/** * 加载动态菜单和路由 */function addDynamicMenuRoute(menuData) {    if (store.state.app.menuRouteLoaded) {        console.log('已加载菜单和路由.')        return    }    // 根据返回的菜单 拼装路由模块    let dynamicRoutes = addDynamicRoutes(menuData)    // 处理静态组件绑定路由    router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)    //添加路由    router.addRoutes(router.options.routes)    // 保存加载状态    store.commit('setMenuLoad', true)}/** * 添加动态(菜单)路由 * @param {*} menuList 菜单列表 * @param {*} routes 递归创建的动态(菜单)路由 */function addDynamicRoutes(menuList = [], routes = []) {    var temp = []    for (var i = 0; i < menuList.length; i++) {        if (menuList[i].children && menuList[i].children.length >= 1) {            temp = temp.concat(menuList[i].children)        } else if (menuList[i].url && /S/.test(menuList[i].url)) {            //将第一个斜杠去掉            menuList[i].url = menuList[i].url.replace(/^//, '')            // 创建路由配置            var route = {                path: menuList[i].url,                component: null,                name: menuList[i].menuName,                meta: {                    title: menuList[i].menuName,                    icon: menuList[i].icon,                    index: menuList[i].menuId,                },            }            try {                // 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储                // 如url="menu/singleMenu/index",则组件路径应是"@/views/menu/singleMenu/index".vue",否则将找不到改组件                let url = menuList[i].url                route['component'] = (resolve) => require([`@/views/${url}`], resolve)            } catch (e) {}            routes.push(route)        }    }    if (temp.length >= 1) {        addDynamicRoutes(temp, routes)    }    return routes}export default router

根据 vuex 中的暂存的菜单生成侧边菜单栏

新建菜单组件 递归生成菜单,新建 menuTree/index.vue ,代码如下:



{{ menu.menuName }} {{ menu.menuName }}




在侧边栏中,从 state 中得到菜单,生成侧边栏菜单,完整代码如下:

退出后重置 vuex

因为只要登录过,那么当前状态中的 活动窗体 肯定是有值的,那么只需要判断该值是否有,有就刷新一下界面。

这里使用的是 reload 来刷新页面。

created() {        //若是使用状态退出 则刷新一下 重置vuex        if (this.$store.state.app.mainTabsActiveName != '') {            window.location.reload()        }    },