前言
后台管理系统中都会存在一个需求:标签栏导航,顶部标签栏为一个个的<router-link>
,再结合<keep-alive>
和<router-view>
实现:
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
然后我们再来监听$route
,来判断当前页面是否需要重新加载或者已被缓存。
方案缺陷
该方案主要来自 vue-element-admin,但其项目中这个方案存在如下2个问题:
- 无法缓存三级以及三级以上路由的问题
- 动态路由页面,同时打开多个详情页(例:路由为
/page/:id
的两个详情页/page/1
,/page/2
),当你使用标签页的刷新功能,刷新/page/1
页面时,/page/2
的页面缓存也会被刷新清除
解决方案
问题1
该问题目前市面上大部分的后台都处理解决了,主要方案基本都是将三级以及三级以上的路由拍平成二级路由,但菜单展示仍然使用原本的路由层级。
// 原路由
const asyncRoutes = [...]
// 降级后的路由,排除component组件字段的深拷贝,不然会导致keep-alive失效
const flatRoutes = getFlatRoutes(deepClone(asyncRoutes, ['component']))
// 三级以及三级以上的路由降级成二级路由
const formatRouter = (routes, basePath = '/', list = [], parent) => {
routes.map(item => {
item.path = path.resolve(basePath, item.path)
const meta = item.meta || {}
if (!meta.parent && parent) {
meta.parent = parent.path
item.meta = meta
}
if (item.redirect) item.redirect = path.resolve(basePath, item.redirect)
if (item.children && item.children.length > 0) {
const arr = formatRouter(item.children, item.path, list, item)
delete item.children
list.concat(arr)
}
list.push(item)
})
return list
}
// 路由降级
export const getFlatRoutes = (routes) => {
return routes.map((child) => {
if (child.children && child.children.length > 0) {
child.children = formatRouter(child.children, child.path, [], child)
}
return child
})
}
问题2
问题复现
这里以 Vben Admin 后台举例,在其 功能 > Tab带参
页面下,进行如下操作:
- 打开
Tab带参1
菜单,在输入框中随便输入值, - 打开
Tab带参2
菜单,也随便输入一个值, - 在顶部标签页中来回切换这两个标签页,可以看到你输入的值都被缓存了下来
- 这时,我们右键其中一个标签页,选择重新加载
- 你会看到另一个标签页的值也被清空了
原因说明
这是因为 vue
的 <keep-alive>
组件的 include 默认是优先匹配组件的 name,使得路由 router 的 name 和路由组件的 name 一一对应,来达到缓存效果,但是因为动态路由,他的 router name 都是一样的,所以你刷新其中一个详情页,另一个详情页缓存的内容也会被清空。
解决方案
在 vue2
中,vue
官方提供了一个vm.$destroy()
api 可以手动销毁组件实例,让我们能在刷新某个标签页的时候单独处理该详情页的缓存,但这个api在vue3
中被移除了,我找到其的替代api $unmounted
,但该api是卸载了整个vue3实例,不再适用了。
要解决该问题,还是要回到 <keep-alive>
组件的 include 原理,要是我们能让组件的 name动态,变成 $route
的fullPath
,那么就可以很优雅的处理这个问题了。
那么如何让组件的 name动态 呢?要知道,我们的组件 name 都是在组件中定义写死的。
很简单:利用vue的渲染函数,给每个组件包一层wrap
就可以啦~
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="wrap(Component)" :key="$route.fullPath" />
</keep-alive>
</router-view>
</template>
<script setup>
const wrap = (fullPath, component) => {
const wrapper = {
name: fullPath,
render() {
return h('div', null, component)
},
}
return h(wrapper)
}
</script>
这样就可以做到动态name了,但是,这里我们又遇到一个问题,在切换标签页时,vue会抛出错误:parentComponent.ctx.deactivate is not a function
为了解决这个问题,我们需要根据cachedViews
数组,缓存多个wrap
,而不是共用一个wrap
,具体代码如下
<template>
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in" appear>
<keep-alive :include="cachedViews">
<component :is="wrap($route.fullPath, Component)" :key="$route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
// 自定义name的壳的集合
const cachedWrapperComponents = new Map()
// 为keep-alive里的component接收的组件包上一层自定义name的壳
const wrap = (fullPath:, component) => {
let wrapper
if (cachedWrapperComponents.has(fullPath)) {
wrapper = cachedWrapperComponents.get(fullPath)
} else {
wrapper = {
name: fullPath,
render() {
return h('div', null, component)
},
}
cachedWrapperComponents.set(fullPath, wrapper)
}
return h(wrapper)
}
// 监听cachedViews的变化,当清除标签页缓存时移除相应的 wapper components
watch(cachedViews, (fullPaths) => {
cachedWrapperComponents.forEach((value, key) => {
if (!fullPaths.includes(key)) {
cachedWrapperComponents.delete(key)
}
})
})
</script>
关于 <keep-alive>
组件的问题就到这里结束了,但有心人可能看到了,渲染函数h('div', null, component)
,我多渲染了一个空的div
,这是因为vue3虽然支持了多根节点元素,但 <transition>
组件要求必须只有1个根节点,不然动画将不会生效。现在你可以在组件中无所顾忌的使用vue3
带来的新特性多根节点啦。
致谢
感谢你抽出宝贵的时间阅读这篇文章,以上说的代码已在我的项目vue-mushroom-admin实现了:
wrap
相关代码位于src/layout/components/AppMain.vue
下- 将三级以及三级以上的路由拍平成二级路由位于
src/store/permission.ts
下
最后,如果觉得这篇文章对你有帮助的话,请给个 star
再走~~~