实现目标
开发后台管理系统做动态路由时,遇到路由超过2级缓存不生效的问题,借此记录下(全是代码没有技巧 滑稽)
示例代码地址:github.com/vela666/vit…
动态路由常见后端返回方式:
- 数据格式类似这样
[
{
menu_id: '0da4b12d84e64764ad3a34d297f799063',
type: 0,
menu_name: '首页',
button_name: '',
menu_sort: 10,
menu_icon: '',
menu_path: '/home',
is_cache: true,
menu_type: 0,
is_enable: 0,
create_time: '2021-08-06 15:33:03',
children: [
{
menu_id: '7e79440386a0416d8e80d03c794b3a355',
type: 0,
menu_name: '引导',
button_name: '',
menu_sort: 3,
menu_icon: '',
menu_path: '/home/user-guide/index',
is_cache: true,
menu_type: 0,
is_enable: 0,
create_time: '2022-08-25 21:22:48',
children: [
{
menu_id: 'a24a3002d3e74357ade602eac897fb317',
type: 0,
menu_name: '概览',
button_name: '',
menu_sort: 1,
menu_icon: '',
menu_path: '/home/overview/index',
is_cache: true,
menu_type: 1,
is_enable: 0,
create_time: '2022-08-08 14:57:51',
children: [],
},
],
},
],
},
]
- 前端写好所有的路由,后端返回对应路由name等类似的标识, 前端根据标识去过滤
如:juejin.cn/post/695231…
路由格式类似这样
// 默认路由
const constantRoutes = [
{
path: '/login',
name: 'Login',
hidden: true,
component: () => import('@/views/login/index.vue'),
meta: {
hidden: true,
title: '登录',
},
}
]
// 根据后端返回的name(标识)过滤展示
const asyncRoutes = [
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: 'Home',
meta: { title: '主页', icon: 'dashboard' },
children: [
{
path: '/dashboard',
component: () => import('@/views/home'),
name: 'Dashboard',
meta: { title: '主页', icon: 'dashboard', affix: true },
},
],
},
{
path: '/nested',
name: 'Nested',
component: Layout,
redirect: '/nested/menu1/menu1-1',
meta: {
title: '嵌套路由',
icon: 'nested',
noCache: false,
roles: ['admin'],
},
children: [
{
path: '/nested/menu1',
name: 'Menu1',
component: () => import('@/views/nested/menu1'),
meta: {
title: 'menu1',
noCache: false,
},
redirect: '/nested/menu1/menu1-1',
children: [
{
path: '/nested/menu1/menu1-1',
component: () => import('@/views/nested/menu1/menu1-1'),
name: 'Menu1-1',
meta: {
title: 'menu1-1',
noCache: false,
},
},
{
path: '/nested/menu1/menu1-2',
name: 'Menu1-2',
redirect: '/nested/menu1/menu1-2/menu1-2-1',
component: () => import('@/views/nested/menu1/menu1-2'),
meta: {
title: 'menu1-2',
noCache: false,
},
children: [
{
path: '/nested/menu1/menu1-2/menu1-2-1',
component: () =>
import('@/views/nested/menu1/menu1-2/menu1-2-1'),
name: 'Menu1-2-1',
meta: {
title: 'menu1-2-1',
noCache: false,
},
},
{
path: '/nested/menu1/menu1-2/menu1-2-2',
component: () =>
import('@/views/nested/menu1/menu1-2/menu1-2-2'),
name: 'Menu1-2-2',
meta: {
title: 'menu1-2-2',
noCache: false,
},
},
],
},
],
},
{
path: '/nested/menu2',
name: 'Menu2',
component: () => import('@/views/nested/menu2'),
meta: {
title: 'menu2',
noCache: false,
// icon : 'devices',
roles: ['admin'],
},
},
{
path: '/nested/menu3',
name: 'Menu3',
component: () => import('@/views/nested/menu3'),
meta: {
title: 'menu3',
noCache: false,
// icon : 'devices',
roles: ['admin'],
},
},
],
}
]
const notFoundRoute = {
path: '/:pathMatch(.*)*',
// 不要写name不然动态路由 页面刷新就在404页面
// name: 'Redirect404',
hidden: true,
meta: {
title: '404',
},
component: () => import('@/views/error/404'),
}
以第一种方式实现(第二种也差不多) vite方式 (webpack也差不多)
import Layout from '@/layout'
const defaultPath = '../../../views/'
// 过滤4|5开头的文件
const dynamicRoutesModules = import.meta.glob('../../../views/**/!(4|5).vue')
// bool = true creative-label false === CreativeLabel
function toPascalCase(str, bool = true) {
const tmp = str.replace(/\/index$/, '').match(/\/([^/]+)\/?$/g)?.[0] ?? str
// const words = tmp.replace('/', '').split('-');
const words = tmp.replace(/^\/+|\/+$/g, '').split('-')
const capitalizedWords = words.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1),
)
// return capitalizedWords.join('-') || tmp; // creative-label === Creative-Label
return capitalizedWords.join(bool ? '' : '-') || tmp // creative-label === CreativeLabel
}
// 父级记录N层子级标识
function getParentPath1(parent, childrenPath, key = 'path') {
parent?.children?.forEach((child) => {
childrenPath.push(child[key])
child?.children && getParentPath1(child, childrenPath, key)
})
}
/**
* @description 映射动态路由(超过二级路由平铺为二级路由,解决keep-alive不缓存问题)
* @param {Array} menus 后端接口返回的菜单列表
* @param {Boolean} level 路由嵌套级别 false 不平铺 true 平铺
* @param {Boolean} needTiling 需不需平铺(渲染左侧或顶部菜单不平铺)
*/
// let firstLoop = true
export function generatorDynamicRoutes1(
menus = [],
isOneLevel = false,
parentPath = [],
needTiling = true,
) {
const routes = []
menus.forEach((item) => {
const route = {
path: item.menu_path,
// 缓存路由时用
name: toPascalCase(item.menu_path),
meta: {
title: item.menu_name,
noCache: !item.is_cache,
id: item.menu_id,
icon: 'menu',
// 添加父级记录所有子级 标识
hasSubs: [],
// affix: false,
// 子级记录所有父级 标识
hasParents: [...parentPath],
},
component: null,
}
getParentPath1(item, route.meta.hasSubs, 'menu_path')
// 去除第一个 / /home/test2/ = home/test2/
// const comp = item.menu_path.replace(/\//, '')
// 去除路径前面和后面的 /
const comp = item.menu_path.replace(/^\/+|\/+$/g, '')
if (item.children && item.children.length > 0) {
// route.component = Layout
// 只有第一层应为Layout 防止子级包含children 时 component 也设置为 Layout
route.component = !isOneLevel
? Layout
: dynamicRoutesModules[`${defaultPath}${comp}.vue`]
route.redirect = item.children[0].menu_path
const parentMark = [...route.meta.hasParents, item.menu_path]
if (isOneLevel && needTiling) {
routes.push(
...generatorDynamicRoutes1(
item.children,
true,
parentMark,
needTiling,
),
)
} else {
route.children = generatorDynamicRoutes1(
item.children,
true,
parentMark,
needTiling,
)
// 第一个路由添加 affix 标识 固定
/*if (firstLoop) {
route.children[0].meta.affix = true
}*/
}
// firstLoop = false
} else {
// comp 路由文件目录位置
// '../../views/b/index.vue'.match(/[^/]*\.vue$/) 获取文件名index.vue
route.component = dynamicRoutesModules[`${defaultPath}${comp}.vue`]
// 打包后运行不了 开发环境可以 之前测试的
// route.component = () => import(/* @vite-ignore */ `${defaultPath}${comp}`)
// route.component = () => import(/* @vite-ignore */ '../../views/' + comp)
// route.component = () => import(/* @vite-ignore */ '/src/views/' + comp)
// webpack方式
// const comp = item.menu_path.replace(/\//, '');
// webpackChunkName: request 占位符 会以文件名来填写名字 index 使用数字
// route.component = import(/* webpackChunkName: "[index]" */ `@/views/${comp}`)
// route.component = () => import(/* webpackChunkName: "[index]" */`@/views/${comp}`)
}
routes.push(route)
})
return routes
}
然后再router.beforeEach里添加即可
router.beforeEach(async (to, from, next) => {
NProgress.start()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// 请求过菜单
if (permissionStore.asyncRoutesRequested) {
next()
} else {
try {
await userStore.getUserInfo()
// 获取菜单
await permissionStore.getMenus()
// permissionStore.generateRoutes = 使用generatorDynamicRoutes1方法处理好的路由
if (permissionStore.generateRoutes.length > 0) {
permissionStore.generateRoutes.forEach((route) => {
router.addRoute(route.name, route)
})
}
next({ ...to, replace: true })
} catch (error) {
await userStore.resetInfo()
next('/login')
}
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.fullPath}`)
}
}
})
添加动态路由时 可能会遇到 添加完成 一直在404页面 或者在动态路由添加完成后再添加404
const notFoundRoute = {
path: '/:pathMatch(.*)*',
// 不要写name不然动态路由 页面刷新就在404页面
// name: 'Redirect404',
hidden: true,
meta: {
title: '404',
},
component: () => import('@/views/error/404'),
}
多级路由缓存效果展示
End~