# 路由页面缓存
# 前言
提醒
在处理类似于‘下拉刷新列表’页面来回跳转时的数据缓存与位置定位问题时,不要尝试Vuex,应该使用Vue内置的keep-alive组件来解决。
申明
文章部分内容来源于网络,基于Vue2.0版本 原文
keep-alive | vue-router |
---|---|
示例 文档 | 文档 |
# 背景
移动端Vue项目的订单列表页面一般使用的是下拉加载,于是每次点击订单进行跳转后,返回列表页时,都会重新开始加载数据,且筛选条件也一并被清空。产品要求改善相关体验,保存用户进入详情时列表数据以及列表下拉位置。
之前使用过 dva.js
,业务数据被细致的分割管理在 Redux Store 中,进行页面跳转时并不会清空数据,下次再次进入页面时只需在生命周期里判断下是否需要重新加载即可。于是这次想当然的想引入Vuex实现类似的效果。
其实vue已经内置了相关的解决方案:keep-alive
。
看keep-alive
相关文档时,看的是其配合component
做Tab切换时使用。思想容易被束缚住了,其实keep-alive
中可以还可以容纳其他组件,用于保持组件的内部状态。
# 实现
# 基本使用(缓存所有路由组件)
<!-- 路由入口 -->
<keep-alive>
<router-view>
<!-- 所有路径匹配到的视图组件都会被缓存! -->
</router-view>
</keep-alive>
# 部分缓存
// routes 配置
export default [
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/:id',
name: 'edit',
component: Edit,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
<!-- 路由入口 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
# 跳转判断(废弃)
实践不可用
切换keepAlive
会导致A组件渲染在两个router-view
中导致页面状态不一致,使用体验混乱,实践证明,开发中不应该切换keepAlive
的值,其只作为标识该页面是否需要缓存的常量const
。
# 需求
路由 A,B,C
A默认显示
B -> A 不刷新
C -> A 刷新
# 实现
- A
// A路由配置
{
path: '/',
name: 'A',
component: A,
meta: {
keepAlive: true // 需要被缓存
}
}
- B
// B组件配置
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; // 让 A 缓存,即不刷新
next();
}
};
- C
// C组件配置
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = false; // 让 A 不缓存,即刷新
next();
}
};
# 生产实践
备注
最终应用的实现方式
# 需求场景
- A,B,C,D,E 五个页面
- A 为主页
- B,C,D 为 二级页面(下拉加载列表...)
- E 为详情页,B,C,D 可访问
- B,C 可直接跳转(不通过A),相互跳转时缓存组件
- D 为不缓存页面
- A 跳转 B,C,D 时都要实时刷新
- B,C 跳转至 E 时缓存
# 代码实现
<!-- 路由入口 -->
<keep-alive v-if="!$route.meta.cancelAlive">
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
// 路由配置
{
path: '/',
name: 'A',
component: A,
meta: {
cancelAlive: true // 切换到该页面时,销毁缓存组件(keep-alive)
}
},
{
path: '/',
name: 'B',
component: B,
meta: {
keepAlive: true // 需要被缓存
}
},
{
path: '/',
name: 'C',
component: C,
meta: {
keepAlive: true // 需要被缓存
}
},
{
path: '/',
name: 'D',
component: D
},
{
path: '/',
name: 'E',
component: E // 详情页面,不要缓存
}
# 解释
keep-alive
实际上是将内部的组件的Status缓存下来,离开时不销毁组件,而是失活组件。对应的再次访问时,组件也不会经历"创建"生命周期,而是会被激活,缓存都存储于keep-alive
组件内。所以要清除缓存的话,可以通过v-if
对keep-alive
进行创建或销毁,达到重置缓存的目的。
如果要进行颗粒度更细的缓存管理,可以考虑在组件内部的导航守卫中,让需要清除缓存的组件自己进行自我销毁,达到刷新/清除缓存的效果。
beforeRouteLeave (to, from, next) {
if(to.name === '特定页面') {
this.$destroy(); // 实现自我销毁,达到清除缓存的目的。
}
next();
}
不可用
经过实践,发现keep-alive不能配合$destroy方法使用,使用该方法后,该组件再次缓存后便再也不能被激活。
解决方案
最终,在项目中引入Vuex,通过绑定keep-alive的include/exclude属性,跳转路由时commit对应事件,以达到控制组件缓存的效果,且实践可用。 (这个方案不智能,最佳方案请往下翻)
# 额外的
一般需要缓存的都是下拉加载页面,自然的,除了数据,进入下一级时当前页面列表的位置(滚动条位置)也是需要被保存的,但是keep-alive
并不会保存原生Dom的相关属性,所以要将scroll
位置的值保存在组件实例内部,下次被激活时通过该值去控制scroll
的位置。
当你使用的是Vue-Router的 HTML5 History 时,可通过 scrollBehavior 路由方法控制滚动条
# 例子
<!-- 组件template区 -->
<div ref="scrollBox">
<!-- 实例通过this.$refs.scrollBox控制scroll,固定高度 -->
...
<!-- 数据列表 -->
</div>
// 组件属性
data() {
return {
scroll:0
}
},
beforeRouteLeave (to, from, next) {
// 离开路由时记录滚动条位置
this.scroll = this.$refs.scrollBox.scrollTop;
next();
},
activated() {
if(this.scroll > 0){
// 激活组件时设置滚动条位置
this.$refs.scrollBox.scrollTop = this.scroll;
this.scroll = 0;
}
},
deactivated(){
// 不要在组件失活时去记录,此时Dom上的scrollTop为0
// this.scroll = this.$refs.scrollBox.scrollTop;
}
# 最佳方案
在掘金社区看见了更加智能的缓存管理方案。
# 路由定义
new Router({
routes: [
{
path: '/',
name: 'index',
component: () => import('./views/keep-alive/index.vue'),
meta: {
deepth: 0.5
}
},
{
path: '/list',
name: 'list',
component: () => import('./views/keep-alive/list.vue'),
meta: {
deepth: 1
keepAlive: true //需要被缓存
}
},
{
path: '/detail',
name: 'detail',
component: () => import('./views/keep-alive/detail.vue'),
meta: {
deepth: 2
}
}
]
})
# 导航守卫定义
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view :include="include" v-if="$route.meta.keepAlive">
</router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive">
</router-view>
export default {
name: "app",
data: () => ({
include: []
}),
watch: {
$route(to, from) {
//如果 要 to(进入) 的页面是需要 keepAlive 缓存的,把 name push 进 include数组
if (to.meta.keepAlive) {
!this.include.includes(to.name) && this.include.push(to.name);
}
//如果 要 form(离开) 的页面是 keepAlive缓存的,
//再根据 deepth 来判断是前进还是后退
//如果是后退
if (from.meta.keepAlive && to.meta.deepth < from.meta.deepth) {
var index = this.include.indexOf(from.name);
index !== -1 && this.include.splice(index, 1);
}
}
}
};
这套方案通过在路由元信息定义页面的深度,然后在导航守卫处加以判断,更加智能的解决了路由缓存管理的问题。(学无止境...)