系统的路由都是由后端保存,在用户登录时返回给前端,前端根据拿到的路由数据进行动态渲染。
添加一个三级路由菜单
先看一下二级菜单的结构如下
// 拿到后端的路由数据后最终转换成如下前端路由所需的格式
import Layout from '@/views/layout/index'
const routes = {
path: '/data-board',
name: 'DataBoard',
component: Layout,
redirect: 'sale',
meta: {
title: '数据看板',
icon: 'board'
},
children: [
{
path: 'sale',
name: 'DataBoardSale',
component: () => import('@/views/data-board/sale'),
meta: {
title: '销售看板'
}
}
]
}
export default routes
AppMain
<template>
<section class="app-main">
<keep-alive v-if="enableCache" :include="cachedViews">
<router-view :key="key" />
</keep-alive>
<router-view v-else />
</section>
</template>
根据需求,首先先配置一个三级路由,结构如下
import Layout from '@/views/layout/index'
const routes = {
path: '/backstage',
name: 'Backstage',
component: Layout,
redirect: '/backstage/sale-board/department',
meta: {
title: '后台管理',
icon: 'backstage'
},
children: [
{
path: 'sale-board',
name: 'BackstageSaleBorad',
redirect: '/backstage/sale-board/department',
component: () => import('@/views/backstage'),
meta: {
title: '销售看板'
},
children: [
{
path: 'department-sale',
name: 'DepartmentSale',
component: () => import('@/views/backstage/department-sale'),
meta: {
title: '事业部与销售部门关系'
}
},
{
path: 'department-sale-target',
name: 'DepartmentSaleTarget',
component: () => import('@/views/backstage/department-sale-target'),
meta: {
title: '事业部销售销售目标'
}
},
...
]
}
]
}
export default routes
views/backstage下需要新建一个空白路由用作二级路由,用来承载三级路由
// views/backstage/index.vue
<template>
<div>
<router-view :key="key" />
</div>
</template>
完整的目录结构
通过以上操作,三级路由就实现了,但问题也来了。。。
三级路由的页面缓存失效了,每次切换回来页面都会重新加载?
vue-element-admin里是通过vuex里的cachedViews来缓存的,每次打开一个新页面便将其push进cachedViews,配合keep-alive达到页面缓存的目的
<keep-alive v-if="enableCache" :include="cachedViews">
<router-view :key="key" />
</keep-alive>
<router-view v-else />
但为什么三级路由的页面缓存失效了呢?查看cachedViews发现三级路由的name也没什么问题。
此时想到,我只缓存了三级路由的页面如‘事业部与销售部门关系’页面DepartmentSale,他的爸爸二级路由‘销售看板’页面BackstageSaleBorad并没有缓存,于是手动将BackstageSaleBorad添加进cachedViews,果然,这时候缓存生效了。
解决方案一:缓存三级路由的同时将其二级父路由添加到cachedViews
由上面可知,三级路由的情况下,只需在打开三级路由时把其对应的二级父路由也加进去cachedViews即可。
但此方案会同时把二级和三级路由缓存,看起来未免有些浪费,而且对代码的改动也较多,需要改动tagsView和vuex里相关的动作,所以我考虑下面的方案👇
解决方案二:将路由与菜单分隔开,菜单显示依旧使用三级结构,路由使用二级
我们知道缓存失效是因为三级结构导致的,那我们将后端返回给我们的带有三级结构的路由copy一份,将其转换为二级路由使用,而界面显示的菜单仍然用原本的三级结构渲染。
采用此方案还可以减少额外的空白路由文件。
路由转换思路:
将三级路由提升到二级路由的位置,原本的二级路由则删除。
注意转换过程中path的变化,这里我把删除的二级父路由的path拼接到其子路由的path
核心代码实现:
// 处理后端component字段
function filterAsyncRouter(asyncRouterMap) {
const accessedRouters = asyncRouterMap.filter(item => {
if (!item.component) return false
// Layout组件特殊处理
if (['layout', 'Layout'].includes(item.component)) {
item.component = Layout
} else {
// component为'null'表示为二级路由,不用转换
if (item.component !== 'null') {
item.component = _import(item.component)
}
}
if (item.children && item.children.length) {
item.children = filterAsyncRouter(item.children)
}
return true
})
return accessedRouters
}
// 转为二级路由
function convertRoutes(accessRoutes) {
const castRoute = (routes) => {
let flatRoutes = []
routes.forEach(item => {
if (item.children && item.children.length) {
item.children.forEach(child => {
flatRoutes.push({
...child,
path: `${item.path}/${child.path}`,
})
})
} else {
flatRoutes.push({
...item
})
}
})
return flatRoutes
}
let result = []
accessRoutes.forEach(accessRouter => {
let childrenRoutes = []
if (accessRouter.children && accessRouter.children.length) {
childrenRoutes = castRoute(accessRouter.children)
}
result.push({
...accessRouter,
children: childrenRoutes
})
})
return result
}
state: {
permissinRoutes: [], // 路由
appMenuList: [] // 菜单
},
mutations: {
SET_PERMISSION_ROUTES: (state, routes) => {
state.permissinRoutes = routes
},
SET_APP_MENU_LIST: (state, list) => {
state.appMenuList = list
},
}
actions: {
generateRoutes({ commit, state }) {
return new Promise((resolve, reject) => {
let appMenuList = []
let permissionRoutes = []
// 服务端返回的菜单
const serverMenuList = state.userInfo.menuList
// 替换component字段为真实的前端组件
const afterReplaceComponent = filterAsyncRouter(serverMenuList)
// 三级路由=>二级路由
permissionRoutes = convertRoutes(afterReplaceComponent)
// 全部菜单(包含三级)
appMenuList = [...constantRoutes, ...afterReplaceComponent]
commit('SET_PERMISSION_ROUTES', permissionRoutes)
commit('SET_APP_MENU_LIST', appMenuList)
resolve(permissionRoutes)
})
}
}
路由拦截里
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
// has token
if (to.path === LOGIN_PATH) {
// 如果是登录页,直接进入首页
next({ path: '/' })
NProgress.done() // 如果当前页是首页时不会触发afterEach hook,需要在这里结束进度条
} else {
if (store.getters.appMenuList.length === 0) {
store.dispatch('generateRoutes').then(routes => {
let arr = [...routes]
arr.push({
path: '*',
redirect: '/404',
hidden: true
})
// console.log('路由', arr)
resetRouter()
router.addRoutes(arr)
next({ ...to, replace: true })
}).catch(err => {
console.error('err', err)
Message.error('路由初始化失败')
NProgress.done() // 如果当前页是首页时不会触发afterEach hook,需要在这里结束进度条
})
} else {
next()
}
}
} else {
// has no token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
// next(`${LOGIN_PATH}?redirect=${to.path}`) // 否则全部重定向到登录页
next(`${LOGIN_PATH}`) // 否则全部重定向到登录页
NProgress.done() // 如果当前页是登录页时不会触发afterEach hook,需要在这里结束进度条
}
}
})
router.afterEach((to) => {
NProgress.done() // finish progress bar
if (to.meta.title) {
document.title = `${to.meta.title}-${projectName}`
}
})
happy ending~~~