Vue如何根据角色获取菜单动态添加路由
作者:劫辞
如果大家写过后台管理系统的项目,那么动态路由一定是绕不开的,如果想偷懒的话,就把所有路由一开始都配置好,然后只根据后端返回的菜单列表渲染就好菜单就好了,但是这样的隐患就是我在地址栏输入的地址的时候,也会进入这个页面,不偷懒的方法就是本文要介绍的,真动态路由了,当然不会仅仅只是介绍使用数据怎么换成动态路由添加就好了,会从登录获取token后请求菜单列表…最后注册完成,这一系列流程完整的实现一次,相信对于第一次接触这个案例的朋友会有帮助
前提提要
- 本文有些东西我不会详细的说,比如后端部分,前端代理啊,基于 element-ui 的递归菜单封装等其他组件使用等等,我不会在做额外的赘述了,后端这个流程包裹这些封装,后面我会单独开一篇文章来说明
- 前端 vue 项目结构部分也不会太过详细的说明,所以观看本文还是需要一定的基础,至少知道vue的基础语法、用过 vue-router 和 vuex 吧,要求还是不高的
需求分析
- 在实现我们这个需求,不难想到主要就是完成登录,通过登录获取到正确的菜单列表,通过菜单列表进行渲染
- 但是完成这个步骤的话,我们还需要捋一下页面的关系,按照我们的开发时态来说,我们启动一个项目之后,会通过 http://localhost:8080/ 这样的一个地址在浏览中打开
- 打开这个地址之后,触发的是什么路径,是不是
/
,表示根路径,在后台管理系统中,一般这个跟路径我们会映射到什么组件上,是不是 layout 组件,比如这样的,如图:
- 但是这样的话就和我们的需求有点不一样了,我们要先登录啊,都没登录怎么能打开这个呢?所以一把来说,我们一般会要么把 ‘/’ 的路径触发时,重定向到 ‘/login’,或者在全局路由前置守卫中,通过登录的状态来决定是不是跳转到登录页,一般我们使用第二种,因为后台管理系统中,一定会有路由权限的判断,到时候一样会来改动这个,所以选择后者,至于实现部分,我们后面再看
- 完成了上述的操作之后,就是登录了,登录之后获取菜单列表数据,拿到之后我们就直接注册吗?
- 我们知道,这种菜单,往往会有一级、二级、三级等等不同级别的菜单,而是不是每个菜单都需要注册的呢,其实不然,我们需要注册的仅仅是需要展示的那一部分菜单,比如在我们的案例中,设备管理是一个一级菜单,但是存在子级菜单,那么此时这个设备管理菜单就是不需要注册的,如图:
- 所以这一点我们也需要做一下区分,但是具体注册那些呢?这些就还是要在前端先配置好,但是这个配置不会是直接配置到 route 中,是一个映射关系,比如定义了 a = 组件A,然后依次书写,把所有会展示的页面通过这样的方式,用一个文件存储起来,那么通过后端返回的菜单列表数据时,就可以进行一个对比,筛选,取出符合条件的数据,组装成一个适配业务的 route 进行注册
- 而通过这样的匹配,我们最后是可以得到一个数组的,[route1, route2, …],得到这个数组之后,使用 vueRouter的 addRoute 方法添加即可
- 这里需要注意的事情是,我所演示的案例中,所有的子组件都是在 main 区域显示的,所以我就不需要在去单独的关心这些子组件的层级关系了,但是如果某个项目中的,层级关系如图:
- 像这种或者更多层级的,就需要额外处理一下 children 属性了,但是方法都是差不的,无非就是数据处理的时候多处理一下,而且一般来说就是两层,最外层第一个 router-view 来展示一级路由(比如登录、404、layout),main 区域的 router-view 展示二级路由(比如 home、my、user…)
- 经过这个分析之后,我们就是能确定,我们要做的事情就是,把这些获取的菜单数据,来找到对应的组件,并把这些组件添加为 layout 组件的子组件,在 main 区域展示
具体实现
配置静态路由
根据上面的粗略的分析,第一步就是创建路由,这一步非常简单,我直接粘贴代码了,如下:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const router = new VueRouter({ mode: 'hash', routes: [] }) export default router
这就是一个最基础的结构了,而在这个需求中,至少有两个路由一定是静态的,一个是 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
添加两个静态路由非常简单吧,然后把这个在 main js 页面引入使用,我就不展示了
路由权限判断
- 上面的配置如果我们直接在浏览器中打开 http://localhost:8080/ 这个地址,那么展示的就是 layout 组件,如果需要展示位 login 组件的话,我们就需要在全局前置路由守卫上动一下手脚了
- 也非常简单,一个用户登没登录,就是判断是否是存在了 token,如果有就是登录了,如果没有就是没有登录,根据这个,我们可以得出一张关系图,如图:
这只是一个简单的路由权限判断,具体的还需要根据业务来扩展,根据这个关系图,我们可以写出如下代码:
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
登录
实现这点的方法也不止一种,本文采用的是在 store 的 login 模块中完成登录,至于 axios 的封装或者基于 xhr 等等的请求方面,我这里不做解析了
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
至于 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') } } }
这部分代码还是非常简单的,在入口文件main.js 引用 store 和在登录界面收集表单数据提交调用这个 login 方法登录,大家就自己实现一下吧
现在我们获取到这个数据之后,表示我们可以完成两件事情,第一就是渲染侧边的菜单列表,第二就是根据这个来添加正确的动态路由
渲染菜单列表没有什么好说的,如果没有菜单栏的递归需求的话,菜单栏直接 cv 组件库的代码即可,需要递归的话就要自己封装一下了
添加动态路由
要添加动态路由,需要有两个数据,一个是远程获取的菜单数据,一个是前端的映射的组件关系。远程数据已经有了,前端映射的组件关系,就看你自己的业务来配置了,还是非常简单的,把你前端需要展示的页面都在一个 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') } ]
具体需要多少配置项,就视个人业务而定,我这里使用 name 匹配,你也可以是 path 或者其他属性
在看一下远程的数据具体是什么样的,有助于理解,如下:
[ { "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 } ] } ]
剩下的就是递归遍历的找出组装出对应的 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) } }
有了这个方法之后,自然就是使用,如下:
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') } } }
此时我们已经完成了整个效果的实现,当然还有一个问题,但是这个问题后面再说,先看一下效果,如图:
可以看到,不同的账户登录会因为角色不同展现的菜单也不同
修复刷新路由丢失问题
现在我们这个看着没什么问题,是因为我们没有点击刷新,先看看问题,如图:
一旦刷新之后就会导致动态路由清空,但是又没有重新注册添加,自然就会找不到这个路由了,因此白屏就很正常了
解决也非常简单,在每次刷新的时候,都在重新注册一次动态路由就好了,所以在 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)) } } } }
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
最后在 main.js 中调用此方法即可,导入和使用语句如下:
import { loadLocal } from './store' loadLocal()
现在我们在来看看效果,如图:
结语
这里只是给大家展示一种思路,具体的实现需要根据自己的业务来定,但是整体的流程都是差不多的
如果对于这个递归菜单,和后端部分这个实现登录逻辑部分,可以查看我后续发布的其他文章,或者如果我没忘记的话,我会来这里补上查看链接
到此这篇关于Vue如何根据角色获取菜单动态添加路由的文章就介绍到这了,更多相关Vue根据角色添加路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!