使用KeepAlive动态缓存标签页
很多管理系统都有已打开的标签功能,类似于现在浏览器的顶部标签栏,打开过的页面一般会缓存一段时间,在这段时间内打开,页面不会重新加载.基于vue 2.x版本的keep-alive标签可以实现缓存页面的需求,但是如何去除缓存呢?
假设这样一个场景,用户打开了标签页-A和标签页-B,停留在标签-A,用户希望重新加载下标签页-B,这个时候keep-alive已经缓存了两个页面,如何单独去掉B页的缓存,使得B页面可以重新进入?
keep-alive的基本功能
可以传入的三个props:
- include 字符串或正则表达式。只有名称匹配的组件会被缓存
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
在 Vue 2.2.0 及其更高版本中,activated 和 deactivated 将会在 ``` 树内的所有嵌套组件中触发。
匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。
存储需要缓存的页面
首先想到的是在路由守卫中把需要的RouteInfo存到Store或者其他什么地方
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | router.beforeEach(async (to, from, next)=> {
 if (
 [
 "Login",
 "Dashboard",
 "NotFound",
 "InternalError",
 "Unauthorized",
 ].includes(to.name)
 ) {
 return next();
 } else {
 if (!getToken()) {
 return next({ name: "Login"});
 } else if (anyMenuMatch(store.state.menuList, to.path)) {
 
 store.commit("ADD_PAGE_TAG", to);
 return next();
 } else {
 return next({ name: "Unauthorized"});
 }
 }
 })
 
 | 
在Vuex中存储已经缓存的页面路由信息,主要使用RouteInfo.path字段来区分页面,使用RouteInfo.meta.title来确定标签页名称,使用RouteInfo.name来确定缓存的组件名称,所有所有的RouteInfo的name字段需要和它import()的组件的name属性一致,如果是使用@vue/composition-api的setup script的组件,需要额外写个普通script,导出一个含name的object.
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | export default new Vuex.Store({
 state: {
 
 keepAlivePages: [],
 },
 mutations:{
 ADD_PAGE_TAG(state, page) {
 const oldIndex = state.keepAlivePages.findIndex(
 (p) => page.path === p.path
 );
 if (oldIndex < 0) {
 state.keepAlivePages.push(page);
 }
 }
 }
 })
 
 | 
在router中指定path,meta,name:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | const constantRoutes = {
 {
 path: "/xxx",
 name: "AComponent",
 meta: {
 title: "仓储",
 },
 component: () => import("/path/to/component/AComponent"),
 }
 }
 
 
 const router = new Router({
 routes: [...constantRoutes, ...functionalRoutes],
 });
 
 export default router;
 
 | 
在Component中指定name:
| 12
 3
 4
 5
 6
 
 | <script>
 export default {
 name: "AComponent",
 };
 </script>
 
 | 
在合适位置展示已访问的页面
使用合适组件展示下用户已经访问的页面标签:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 
 | <template><div
 class="flex justify-start px-5 space-x-1"
 v-if="keepAlivePages && keepAlivePages.length >= 2"
 >
 <el-tag
 v-for="(page, index) of keepAlivePages"
 :key="page.path"
 closable
 class="cursor-pointer rounded-b-none"
 @click="handleNavigate(page)"
 >
 {{ page.meta.title }}
 </el-tag>
 </div>
 <div v-else></div>
 </template>
 <script setup>
 import { useStore, useRoute, useRouter } from "@/composables";
 import { ref, computed } from "@vue/composition-api";
 const store = useStore();
 const route = useRoute();
 const router = useRouter();
 const keepAlivePages = computed(() => {
 return store.state.keepAlivePages;
 });
 
 function handleNavigate(page) {
 if (route.value.path !== page.path) {
 router.push(page);
 }
 }
 </script>
 
 
 | 

移除已缓存的页面
<keep-alive>标签的exclude优先级比include高,我们可以把手工移除(点叉叉)的页面加到exclude,也就是维护一个include列表一个exclude列表,新访问页面的时候吧页面的RouteInfopush到include列表,并且从exclude列表移除相同path的(如果有的话),叉掉页面时,把关掉的页面RouteInfopush到exclude列表,并且从include列表移除相同path的(如果有的话),看下在Vuex mutations中的简单实现:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 
 | export default new Vuex.Store({
 state: {
 
 keepAlivePages: [],
 excludeKeepAlivePages: [],
 },
 mutations:{
 ADD_PAGE_TAG(state, page) {
 const oldIndex = state.keepAlivePages.findIndex(
 (p) => page.path === p.path
 );
 if (oldIndex < 0) {
 state.keepAlivePages.push(page);
 }
 const deleteIndex = state.excludeKeepAlivePages.findIndex(
 (p) => page.path === p.path
 );
 if (deleteIndex >= 0) {
 state.excludeKeepAlivePages.splice(deleteIndex, 1);
 }
 },
 REMOVE_PAGE_TAG(state, page) {
 const deleteIndex = state.keepAlivePages.findIndex(
 (p) => page.path === p.path
 );
 if (deleteIndex >= 0) {
 state.keepAlivePages.splice(deleteIndex, 1);
 }
 const addIndex = state.excludeKeepAlivePages.findIndex(
 (p) => page.path === p.path
 );
 if (addIndex < 0) {
 state.excludeKeepAlivePages.push(page);
 }
 },
 }
 })
 
 
 | 
然后在标签页组件中处理下关闭标签的事件:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 
 | 
 const keepAlivePages = computed(() => {
 return store.state.keepAlivePages;
 });
 
 
 function handleCloseSelf(page, index) {
 const allPages = keepAlivePages.value;
 if (allPages.length === 1) {
 return router.push({ name: "Dashboard" });
 }
 if (index >= 1) {
 return router.push(allPages[index - 1]);
 }
 if (index === 0) {
 return router.push(allPages[index + 1]);
 }
 }
 
 
 function handleClosePageTag(page, index) {
 if (route.value.path === page.path) {
 handleCloseSelf(page, index);
 }
 store.commit("REMOVE_PAGE_TAG", page);
 }
 
 | 
keep-alive路由部分
在需要缓存页面的router-view节点上加keep-alive,这里我还加了开关用于动态启用关闭缓存功能,也是存在vuex中的一个备份值openPageKeepAlive.
| 12
 3
 4
 5
 6
 
 | <keep-alive:include="openPageKeepAlive ? keepAlivedNames : []"
 :exclude="openPageKeepAlive ? excludeKeepAlivedNames : []"
 >
 <router-view/>
 </keep-alive>
 
 |