Vue-根据角色获取菜单动态添加路由

文章目录

    • 前提提要
    • 需求分析
    • 具体实现
      • 配置静态路由
      • 路由权限判断
      • 登录
      • 添加动态路由
      • 修复刷新路由丢失问题
    • 结语

如果大家写过后台管理系统的项目,那么动态路由一定是绕不开的,如果想偷懒的话,就把所有路由一开始都配置好,然后只根据后端返回的菜单列表渲染就好菜单就好了,但是这样的隐患就是我在地址栏输入的地址的时候,也会进入这个页面,不偷懒的方法就是本文要介绍的,真动态路由了,当然不会仅仅只是介绍使用数据怎么换成动态路由添加就好了,会从登录获取token后请求菜单列表…最后注册完成,这一系列流程完整的实现一次,相信对于第一次接触这个案例的朋友会有帮助

前提提要

  1. 本文有些东西我不会详细的说,比如后端部分,前端代理啊,基于 element-ui 的递归菜单封装等其他组件使用等等,我不会在做额外的赘述了,后端这个流程包裹这些封装,后面我会单独开一篇文章来说明
  2. 前端 vue 项目结构部分也不会太过详细的说明,所以观看本文还是需要一定的基础,至少知道vue的基础语法、用过 vue-router 和 vuex 吧,要求还是不高的

需求分析

  1. 在实现我们这个需求,不难想到主要就是完成登录,通过登录获取到正确的菜单列表,通过菜单列表进行渲染

  2. 但是完成这个步骤的话,我们还需要捋一下页面的关系,按照我们的开发时态来说,我们启动一个项目之后,会通过 http://localhost:8080/ 这样的一个地址在浏览中打开

  3. 打开这个地址之后,触发的是什么路径,是不是 /,表示根路径,在后台管理系统中,一般这个跟路径我们会映射到什么组件上,是不是 layout 组件,比如这样的,如图:

    Vue-根据角色获取菜单动态添加路由_第1张图片

  4. 但是这样的话就和我们的需求有点不一样了,我们要先登录啊,都没登录怎么能打开这个呢?所以一把来说,我们一般会要么把 ‘/’ 的路径触发时,重定向到 ‘/login’,或者在全局路由前置守卫中,通过登录的状态来决定是不是跳转到登录页,一般我们使用第二种,因为后台管理系统中,一定会有路由权限的判断,到时候一样会来改动这个,所以选择后者,至于实现部分,我们后面再看

  5. 完成了上述的操作之后,就是登录了,登录之后获取菜单列表数据,拿到之后我们就直接注册吗?

  6. 我们知道,这种菜单,往往会有一级、二级、三级等等不同级别的菜单,而是不是每个菜单都需要注册的呢,其实不然,我们需要注册的仅仅是需要展示的那一部分菜单,比如在我们的案例中,设备管理是一个一级菜单,但是存在子级菜单,那么此时这个设备管理菜单就是不需要注册的,如图:

    Vue-根据角色获取菜单动态添加路由_第2张图片

  7. 所以这一点我们也需要做一下区分,但是具体注册那些呢?这些就还是要在前端先配置好,但是这个配置不会是直接配置到 route 中,是一个映射关系,比如定义了 a = 组件A,然后依次书写,把所有会展示的页面通过这样的方式,用一个文件存储起来,那么通过后端返回的菜单列表数据时,就可以进行一个对比,筛选,取出符合条件的数据,组装成一个适配业务的 route 进行注册

  8. 而通过这样的匹配,我们最后是可以得到一个数组的,[route1, route2, …],得到这个数组之后,使用 vueRouter的 addRoute 方法添加即可

  9. 这里需要注意的事情是,我所演示的案例中,所有的子组件都是在 main 区域显示的,所以我就不需要在去单独的关心这些子组件的层级关系了,但是如果某个项目中的,层级关系如图:

    Vue-根据角色获取菜单动态添加路由_第3张图片

  10. 像这种或者更多层级的,就需要额外处理一下 children 属性了,但是方法都是差不的,无非就是数据处理的时候多处理一下,而且一般来说就是两层,最外层第一个 router-view 来展示一级路由(比如登录、404、layout),main 区域的 router-view 展示二级路由(比如 home、my、user…)

  11. 经过这个分析之后,我们就是能确定,我们要做的事情就是,把这些获取的菜单数据,来找到对应的组件,并把这些组件添加为 layout 组件的子组件,在 main 区域展示

具体实现

配置静态路由

  1. 根据上面的粗略的分析,第一步就是创建路由,这一步非常简单,我直接粘贴代码了,如下:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    
    const router = new VueRouter({
    	mode: 'hash',
    	routes: []
    })
    
    export default router
    
  2. 这就是一个最基础的结构了,而在这个需求中,至少有两个路由一定是静态的,一个是 login,一个是 layout,当然通常还有个一个任意路由,表示 404,这里我就不写了,大家有时间自己添加一下就好,如下:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    
    const router = new VueRouter({
    	mode: 'hash',
    	routes: [
    		{
    			path: '/',
    			name: 'layout',
    			component: () => import('@/layout')
    		},
    		{
    			path: '/login',
    			name: 'login',
    			component: () => import('@/views/login')
    		}
    	]
    })
    
    export default router
    
  3. 添加两个静态路由非常简单吧,然后把这个在 main js 页面引入使用,我就不展示了

路由权限判断

  1. 上面的配置如果我们直接在浏览器中打开 http://localhost:8080/ 这个地址,那么展示的就是 layout 组件,如果需要展示位 login 组件的话,我们就需要在全局前置路由守卫上动一下手脚了

  2. 也非常简单,一个用户登没登录,就是判断是否是存在了 token,如果有就是登录了,如果没有就是没有登录,根据这个,我们可以得出一张关系图,如图:

    Vue-根据角色获取菜单动态添加路由_第4张图片

  3. 这只是一个简单的路由权限判断,具体的还需要根据业务来扩展,根据这个关系图,我们可以写出如下代码:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    
    import store from '@/store'
    
    const router = new VueRouter({
    	mode: 'hash',
    	routes: [
    		{
    			path: '/',
    			name: 'layout',
    			component: () => import('@/layout')
    		},
    		{
    			path: '/login',
    			name: 'login',
    			component: () => import('@/views/login')
    		}
    	]
    })
    
    router.beforeEach((to, from, next) => {
    	const token = store.state.login.token
    	if (token) {
    		if (to.path === '/login') {
    			next(false)
    		} else {
    			next()
    		}
    	} else {
    		if (to.path === '/login') {
    			next()
    		} else {
    			next('/login')
    		}
    	}
    })
    
    export default router
    

登录

  1. 实现这点的方法也不止一种,本文采用的是在 store 的 login 模块中完成登录,至于 axios 的封装或者基于 xhr 等等的请求方面,我这里不做解析了

  2. store 的基础配置不做赘述了,直接粘贴代码,如下:

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex)
    
    import login from './login'
    
    const store = new Vuex.Store({
    	modules: { login }
    })
    
    export default store
    
  3. 至于 login 模块的话,书写也非常简单,编写登录函数,登录成功之后同步获取菜单数据,如下:

    import { loginApi, menuApi } from '@/api'
    import router from '@/router'
    
    export default {
    	namespaced: true,
    	state: {
    		userInfo: {} || localStorage.getItem('user_info'),
    		token: '' || localStorage.getItem('token'),
    		menuList: [] || localStorage.getItem('menu_list')
    	},
    	mutations: {
    		SET_MENU_LIST(state, payload) {
    			state.menuList = payload
    		},
    
    		SET_USER_INFO(state, payload) {
    			state.userInfo = payload
    			localStorage.setItem('user_info', JSON.stringify(payload))
    		},
    
    		SET_TOKEN(state, payload) {
    			state.token = payload
    			localStorage.setItem('token', payload)
    		},
            
            // 退出登录
    		LOG_OUT() {
    			localStorage.removeItem('token')
    			localStorage.removeItem('user_info')
    			localStorage.removeItem('menu_list')
    			// 刷新页面-因为路由权限的存在会导航到login,并且通过这个刷新可以避免重复添加路由
    			window.location.reload()
    		}
    	},
    	actions: {
    		async login({ commit }, payload) {
                // 登录请求-获取token
    			const loginResp = await loginApi.reqLogin(payload)
    			if (loginResp?.errorCode !== 0) return
    			commit('SET_USER_INFO', loginResp.data.userInfo)
    			commit('SET_TOKEN', loginResp.data.token)
    
    			// 请求菜单列表
    			const menuListResp = await menuApi.reqGetMenuList()
    			localStorage.setItem('menu_list', JSON.stringify(menuListResp.data))
    			commit('SET_MENU_LIST', menuListResp.data)
    
    			// 跳转至首页
    			router.push('/home')
    		}
    	}
    }
    
    
  4. 这部分代码还是非常简单的,在入口文件main.js 引用 store 和在登录界面收集表单数据提交调用这个 login 方法登录,大家就自己实现一下吧

  5. 现在我们获取到这个数据之后,表示我们可以完成两件事情,第一就是渲染侧边的菜单列表,第二就是根据这个来添加正确的动态路由

  6. 渲染菜单列表没有什么好说的,如果没有菜单栏的递归需求的话,菜单栏直接 cv 组件库的代码即可,需要递归的话就要自己封装一下了

添加动态路由

  1. 要添加动态路由,需要有两个数据,一个是远程获取的菜单数据,一个是前端的映射的组件关系。远程数据已经有了,前端映射的组件关系,就看你自己的业务来配置了,还是非常简单的,把你前端需要展示的页面都在一个 js 文件引入就好了,如下:

    export default [
    	{
    		name: 'home',
    		component: () => import('@/views/home')
    	},
    	{
    		name: 'my',
    		component: () => import('@/views/my')
    	},
    	{
    		name: 'device-add',
    		component: () => import('@/views/device/add')
    	},
    	{
    		name: 'device-list',
    		component: () => import('@/views/device/list')
    	},
    	{
    		name: 'user-add',
    		component: () => import('@/views/user/add')
    	},
    	{
    		name: 'user-list',
    		component: () => import('@/views/user/list')
    	}
    ]
    
  2. 具体需要多少配置项,就视个人业务而定,我这里使用 name 匹配,你也可以是 path 或者其他属性

  3. 在看一下远程的数据具体是什么样的,有助于理解,如下:

    [
        {
            "id": 1,
            "name": "home",
            "path": "/home",
            "nickname": "首页",
            "type": 2,
            "order": 1,
            "parentId": 0,
            "icon": "icon-tubiao_shouye-",
            "children": null
        },
        {
            "id": 2,
            "name": "device",
            "path": "/device",
            "nickname": "设备管理",
            "type": 1,
            "order": 2,
            "parentId": 0,
            "icon": "icon-guanli",
            "children": [
                {
                    "id": 3,
                    "name": "device-list",
                    "path": "/device/list",
                    "nickname": "设备列表",
                    "type": 2,
                    "order": 1,
                    "parentId": 2,
                    "icon": "icon-xuanzeweixuanze",
                    "children": null
                },
                {
                    "id": 4,
                    "name": "device-add",
                    "path": "/device/add",
                    "nickname": "设备添加",
                    "type": 2,
                    "order": 2,
                    "parentId": 2,
                    "icon": "icon-xuanzeweixuanze",
                    "children": null
                }
            ]
        },
        {
            "id": 5,
            "name": "my",
            "path": "/my",
            "nickname": "个人中心",
            "type": 2,
            "order": 3,
            "parentId": 0,
            "icon": "icon-xiazai",
            "children": null
        },
        {
            "id": 6,
            "name": "user",
            "path": "/user",
            "nickname": "用户管理",
            "type": 1,
            "order": 4,
            "parentId": 0,
            "icon": "icon-yonghuguanli",
            "children": [
                {
                    "id": 7,
                    "name": "user-list",
                    "path": "/user-list",
                    "nickname": "用户列表",
                    "type": 2,
                    "order": 1,
                    "parentId": 6,
                    "icon": "icon-xuanzeweixuanze",
                    "children": null
                },
                {
                    "id": 8,
                    "name": "user-add",
                    "path": "/user-add",
                    "nickname": "用户添加",
                    "type": 2,
                    "order": 2,
                    "parentId": 6,
                    "icon": "icon-xuanzeweixuanze",
                    "children": null
                }
            ]
        }
    ]
    
  4. 剩下的就是递归遍历的找出组装出对应的 route 配置的事情了,那么我们需要有这样的一个函数,来帮助我们完成这件事情,代码如下:

    import router from '@/router'
    
    // 前端映射的组件关系配置
    import routeConfig from '@/router/route-config'
    
    export default function (menuList) {
    	const routeList = []
    	const deepMenu = menuList => {
    		for (const menu of menuList) {
    			if (menu.children && menu.children.length > 0) {
    				deepMenu(menu.children)
    			} else {
    				const item = routeConfig.find(item => item.name === menu.name)
    				if (!item) return
    				// 去掉第一项斜杠-子路由 path 属性不需要携带开头的 /
    				const path = menu.path.replace(/^\//, '')
                    // 路由元信息可以帮助我们完成一些其他操作的时候,需要的一些辅助数据
    				routeList.push({ ...item, path, meta: { title: menu.nickname } })
    			}
    		}
    	}
    	deepMenu(menuList)
    
    	for (const route of routeList) {
            // 遍历添加路由
    		router.addRoute('layout', route)
    	}
    }
    
  5. 有了这个方法之后,自然就是使用,如下:

    import { loginApi, menuApi } from '@/api'
    import router from '@/router'
    import menuToRoute from '@/utils/menu-to-route'
    
    export default {
    	namespaced: true,
    	state: {
    		userInfo: {} || localStorage.getItem('user_info'),
    		token: '' || localStorage.getItem('token'),
    		menuList: [] || localStorage.getItem('menu_list')
    	},
    	mutations: {
    		SET_MENU_LIST(state, payload) {
    			state.menuList = payload
                // 调用菜单转路由方法
    			menuToRoute(payload)
    		},
    
    		SET_USER_INFO(state, payload) {
    			state.userInfo = payload
    			localStorage.setItem('user_info', JSON.stringify(payload))
    		},
    
    		SET_TOKEN(state, payload) {
    			state.token = payload
    			localStorage.setItem('token', payload)
    		},
    		
            // 退出登录
    		LOG_OUT() {
    			localStorage.removeItem('token')
    			localStorage.removeItem('user_info')
    			localStorage.removeItem('menu_list')
    			// 刷新页面-因为路由权限的存在会导航到login,并且通过这个刷新可以避免重复添加路由
    			window.location.reload()
    		}
    	},
    	actions: {
    		async login({ commit }, payload) {
    			const loginResp = await loginApi.reqLogin(payload)
    			if (loginResp?.errorCode !== 0) return
    			commit('SET_USER_INFO', loginResp.data.userInfo)
    			commit('SET_TOKEN', loginResp.data.token)
    
    			// 请求菜单列表
    			const menuListResp = await menuApi.reqGetMenuList()
    			localStorage.setItem('menu_list', JSON.stringify(menuListResp.data))
    			commit('SET_MENU_LIST', menuListResp.data)
    
    			// 跳转至首页
    			router.push('/home')
    		}
    	}
    }
    
  6. 此时我们已经完成了整个效果的实现,当然还有一个问题,但是这个问题后面再说,先看一下效果,如图:

    Vue-根据角色获取菜单动态添加路由_第5张图片

    Vue-根据角色获取菜单动态添加路由_第6张图片

  7. 可以看到,不同的账户登录会因为角色不同展现的菜单也不同

修复刷新路由丢失问题

  1. 现在我们这个看着没什么问题,是因为我们没有点击刷新,先看看问题,如图:

    Vue-根据角色获取菜单动态添加路由_第7张图片

  2. 一旦刷新之后就会导致动态路由清空,但是又没有重新注册添加,自然就会找不到这个路由了,因此白屏就很正常了

  3. 解决也非常简单,在每次刷新的时候,都在重新注册一次动态路由就好了,所以在 store 的 login 模块多添加一个方法,如下:

    import { loginApi, menuApi } from '@/api'
    import router from '@/router'
    import menuToRoute from '@/utils/menu-to-route'
    
    export default {
    	namespaced: true,
    	state: {
    		userInfo: {} || localStorage.getItem('user_info'),
    		token: '' || localStorage.getItem('token'),
    		menuList: [] || localStorage.getItem('menu_list')
    	},
    	mutations: {
    		SET_MENU_LIST(state, payload) {
    			state.menuList = payload
    			menuToRoute(payload)
    		},
    
    		SET_USER_INFO(state, payload) {
    			state.userInfo = payload
    			localStorage.setItem('user_info', JSON.stringify(payload))
    		},
    
    		SET_TOKEN(state, payload) {
    			state.token = payload
    			localStorage.setItem('token', payload)
    		},
    
    		LOG_OUT() {
    			localStorage.removeItem('token')
    			localStorage.removeItem('user_info')
    			localStorage.removeItem('menu_list')
    			window.location.reload()
    		}
    	},
    	actions: {
    		async login({ commit }, payload) {
    			const loginResp = await loginApi.reqLogin(payload)
    			if (loginResp?.errorCode !== 0) return
    			commit('SET_USER_INFO', loginResp.data.userInfo)
    			commit('SET_TOKEN', loginResp.data.token)
    
    			const menuListResp = await menuApi.reqGetMenuList()
    			localStorage.setItem('menu_list', JSON.stringify(menuListResp.data))
    			commit('SET_MENU_LIST', menuListResp.data)
    
    			router.push('/home')
    		},
    
    		// 加载本地数据
    		async loadLocal({ commit }) {
    			const menuList =  localStorage.getItem('menu_list')
    			if (menuList) {
    				commit('SET_MENU_LIST', JSON.parse(menuList))
    			}
    		}
    	}
    }
    
  4. loadLocal 这个方法还可以初始化一下其他你需要初始化的信息,包括但不限于这个菜单列表,其他是导出这个方法,让其他人使用,可以直接从这个模块使用,也可以其他地方导出,我这里就在 store/index.js 文件下导出,如下:

    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex)
    
    import login from './login'
    
    const store = new Vuex.Store({
    	modules: { login }
    })
    
    // 导出方法
    export function loadLocal() {
    	store.dispatch('login/loadLocal')
    }
    
    export default store
    
  5. 最后在 main.js 中调用此方法即可,导入和使用语句如下:

    import { loadLocal } from './store'
    
    loadLocal()
    
  6. 现在我们在来看看效果,如图:

    Vue-根据角色获取菜单动态添加路由_第8张图片

结语

这里只是给大家展示一种思路,具体的实现需要根据自己的业务来定,但是整体的流程都是差不多的

如果对于这个递归菜单,和后端部分这个实现登录逻辑部分,可以查看我后续发布的其他文章,或者如果我没忘记的话,我会来这里补上查看链接

你可能感兴趣的:(vue.js,前端,javascript)