# 路由页面缓存

# 前言

提醒

在处理类似于‘下拉刷新列表’页面来回跳转时的数据缓存与位置定位问题时,不要尝试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-ifkeep-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;
}

# 最佳方案

在掘金社区看见了更加智能的缓存管理方案。

Vue路由按需加载

# 路由定义

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);
      }
    }
  }
};

这套方案通过在路由元信息定义页面的深度,然后在导航守卫处加以判断,更加智能的解决了路由缓存管理的问题。(学无止境...)