目录
- 为什么要使用动态路由?
- 添加菜单接口 及 菜单状态管理根据得到的菜单生成动态路由根据 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() } },