生活中的Design.

使用KeepAlive动态缓存标签页

字数统计: 1.4k阅读时长: 6 min
2022/07/06 Share

使用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> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

在 Vue 2.2.0 及其更高版本中,activated 和 deactivated 将会在 ``` 树内的所有嵌套组件中触发。

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。

存储需要缓存的页面

首先想到的是在路由守卫中把需要的RouteInfo存到Store或者其他什么地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//router-guard.js
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//store.js
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//router.js
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:

1
2
3
4
5
6
// AComponent.vue
<script>
export default {
name: "AComponent",
};
</script>

在合适位置展示已访问的页面

使用合适组件展示下用户已经访问的页面标签:

1
2
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>

page1

移除已缓存的页面

<keep-alive>标签的exclude优先级比include高,我们可以把手工移除(点叉叉)的页面加到exclude,也就是维护一个include列表一个exclude列表,新访问页面的时候吧页面的RouteInfopush到include列表,并且从exclude列表移除相同path的(如果有的话),叉掉页面时,把关掉的页面RouteInfopush到exclude列表,并且从include列表移除相同path的(如果有的话),看下在Vuex mutations中的简单实现:

1
2
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
//store.js
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);
}
},
}
})

然后在标签页组件中处理下关闭标签的事件:

1
2
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
//Tag.vue

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.

1
2
3
4
5
6
<keep-alive
:include="openPageKeepAlive ? keepAlivedNames : []"
:exclude="openPageKeepAlive ? excludeKeepAlivedNames : []"
>
<router-view/>
</keep-alive>
CATALOG
  1. 1. 使用KeepAlive动态缓存标签页
    1. 1.1. keep-alive的基本功能
    2. 1.2. 存储需要缓存的页面
    3. 1.3. 在合适位置展示已访问的页面
    4. 1.4. 移除已缓存的页面
    5. 1.5. keep-alive路由部分