使用keepAlive遇到的坑
[[toc]]
介绍
keep-alive是vue的一个内置组件。它会缓存不活动的组件实例,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。它提供了include与exclude属性,允许组件有条件地进行缓存,其中exclude的优先级比include高,max最多可以缓存多少组件实例。
keep-alive详解文档
使用 keep-alive的话会增加两个钩子函数, activated 和 deactivated
下面的文章我是keep-alive配合vue-router一块使用的,当前keep-alive也可以缓存单个组件,在这里就不多赘述。
include和exclude
include 和 exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示详细健文档
我这里使用的vuex配合数组动态控制
<keep-alive :include="includes" exclude="" :max="10" >    <router-view/> </keep-alive>
  <script>  export default { 	computed: { 	  includes() { 	     return state => state.router.includes 	   } 	}, 	methods: {      changeStoreIncludes() {        this.$store.commit('changeIncludes', 'tableLists');      }    }    } </script>  
 
  mutations: { 	changeIncludes(state, payload) { 	  state.includes = payload 	}  }
   | 
 
include和exclude无效问题
使用include/exclude 属性需要给所有vue类的name赋值(注意不是给route的name赋值),否则 include/exclude不生效
export default {  name:'TableList',   data () {   return {}   }, }
  | 
 
直接使用v-if做区分
<transition enter-active-class="animated zoomInLeft" leave-active-class="animated zoomOutRight">      <keep-alive>           <router-view v-if="$route.meta.keepAlive">           </router-view>       </keep-alive> </transition> <transition enter-active-class="animated zoomInLeft" leave-active-class="animated zoomOutRight">      <router-view v-if="!$route.meta.keepAlive">        </router-view> </transition>
   | 
 
这样做的话更加简单明了,而且配合动画更搭,不用再vue组件里面声明name,但是要在route的meta里面添加 {keepAlive:true}字段,如果路由是后台控制的话,前端调试就比较鸡肋。
暴露的问题
问题1:
位置公用的问题,当前列表页跳转到详情页面的时候,使用路由回到列表也时候,会出现位置公用的情况。(如果使用浏览器的回退方式,不会出现位置公用的情况。)
对于这个位置公用的情况,我是一头雾水,期待大佬解答 🤝,有几点要说的不知对错,待求证。
- 如果有数据请求的话,浏览器将会把页面置顶?
 
- 如果是静态页面的话,浏览器会滚到你之前滚动的地方?
 
- 上面仅仅是使用的浏览器的跳转行为,如果使用href或者路由封装一些方法,则都会置顶?
 
- 基于SPA模式开发,所以页面仅有一个,实现页面切换是利用哈希与组件的映射关系,vue-router是通过哈希来模拟完整的url,但是对于页面来说仍是一个url,所以在任何一个组件滚动页面,切换到其他组件的时候,页面仍保持滚动之前的状态,这就是出现位置公用的情况.
 
问题2:
加入有A B C三个页面,我现在只想要A->B时A缓存,然后B->A时,展示缓存的页面,C->A、A->B->C->A等都不要缓存。
对于 问题1 我的想法是,跳转前直接把滚动高度缓存起来,然后每次再回来的时候,在把高度在重新赋值给它,但是如果我页面特别多都需要使用缓存的时候,就有些麻烦,然后我发现路由提供的一个这样的方法。
scrollBehavior文档详解
scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
const router = new VueRouter({   mode: 'hash',   routes,   scrollBehavior(to, from, savedPosition) {          console.log(savedPosition);               if (savedPosition) {       return savedPosition     } else {       if (from.meta.keepAlive) {                  from.meta.scrollTop = document.documentElement.scrollTop;       }       return {x: 0, y: to.meta.scrollTop || 0}     }   }, });
  export default router
 
  scrollBehavior(to, from, savedPosition) {   if (savedPosition) {     return new Promise((resolve, reject) => {       setTimeout(() => {         resolve(savedPosition)       }, 20)     })   } else {     if (from.meta.keepAlive) {       from.meta.scrollTop = document.documentElement.scrollTop;     }     return {x: 0, y: to.meta.scrollTop || 0}   } }
  | 
 
实现返回不刷新、其他菜单进入刷新
实现方式一
 <keep-alive>    <router-view v-if="$route.meta.keepAlive" /> </keep-alive>
  <router-view v-if="!$route.meta.keepAlive" />
 
  | 
 
 {    path: '/table-list',    name: 'table-list',    component: TableList,    meta: {keepAlive: true}   },  {    path: '/table-detail',    name: 'table-detail',    component: () => import('../views/table-detail.vue'),  }
 
  | 
 
方式一和方式二都是基于上面这两个代码段。
在要缓存的列表页添加下面的代码
activated() {      if (this.hasFirst) return   this.queryList() }, beforeRouteLeave(to, from, next) {   if (to.name === 'table-detail') {     if (!from.meta.keepAlive) {       from.meta.keepAlive = true;     }   } else {     from.meta.keepAlive = false;     this.$destroy();    }   next() }
  | 
 
完成上面的代码后,A->B->A正常,然后当A->C->A->B->A发现列表页A不会再缓存了,每次都是新的页面。谷歌后的方法是若不是第一次进入就强制刷新一次缓存页面。
this.$destroy()  调用distory之后不能再缓存该组件 而且会不断进入这个页面后重复生成多个虚拟dom.
继续完善,在main.js中使用router.afterEach((to,from)=>{})
 router.afterEach((to, from) => {      if (from.name && from.name !== 'table-detail' && to.name === 'table-list') {     let isRefresh = sessionStorage.getItem('isRefresh')     if (isRefresh === '0') {       setTimeout(() => {         window.location.reload()       })       sessionStorage.setItem('isRefresh', null)     } else {       sessionStorage.setItem('isRefresh', '0')     }   } else if (from.name === 'table-list' && to.name === 'table-detail') {     sessionStorage.setItem('isRefresh', null)   } else {     sessionStorage.setItem('isRefresh', '0')   } })
 
  | 
 
我不知道谷歌出来的解决方案为什么都在详情页刷新,这样的问题就是用户第一次跳转到详情页,再回到列表页是没有缓存的功能,第二次就会正常,但是客户很可能就会执行这一次操作;
这种解决方式太过原始,用户体验太差,而且需要缓存多个页面就不太好控制,不建议用这个方法
实现方式二
灵感来自方式一,我可以其他方式模拟页面刷新。
<template>     <!--这里一定要使用v-if,好处是你可以使用$nextTick体验更好,另一方面是在使用v-show之后,他就相当于隐藏了该页面,但是如果里面有一些不会diff的dom,就会展示出来,模拟刷新的体验就不太好。例如使用 input->     <div v-if="isRouterAlive">         <div>{{ddd}}</div>         <input v-model="ddd" type="text" />         <table-list ref="table" :multiple="true" :otherTableParams="otherTableParams" :tableColumn="column"/>     </div> </template> <script>   export default {          activated() {       if (this.$route.meta.isRefresh) {          const resetData = this.$options.data()          delete resetData.column  
          Object.assign(this.$data, resetData)          this.isRouterAlive = false          this.$nextTick(function () {            window.scroll(0, 0)            this.isRouterAlive = true          })         setTimeout(() => {           this.queryList()          })       }     },     beforeRouteLeave(to, from, next) {              from.meta.isRefresh = to.name !== 'table-detail';       next()      }   } </script>
   | 
 
优化
现在的代码有两个问题
一是从详情页到列表页,数据不会更新,如果我在详情页修改了某个数据,然后再到列表页就会滞后;
二是从详情页跳转到别的列表页然后在跳转到缓存的列表页,然后他还是会缓存之前的数据,不会更新当前页面;
优化如下:
activated() {      if (this.hasFirst) return  if (this.$route.meta.isRefresh) {     const data = this.$options.data()     delete data.column
      Object.assign(this.$data, data)     this.isRouterAlive = false     this.$nextTick(function () {       window.scroll(0, 0)       this.isRouterAlive = true     })     setTimeout(() => {       this.queryList()     })   } else if (this.$route.meta.isRefresh === false) {           this.queryList()   } }
  beforeRouteEnter(to, from, next) {    to.meta.isRefresh = from.name && from.name !== 'table-detail';   next() },
 
 
 
 
 
  | 
 
最后可以把这些代码抽离成一个mixins,然后编写一个刷新的组件,哪里用在哪里调取下,也可以看这篇文章。
实现方式三
这种用 keep-alive 提供的 include 和 exclude,然后配合vuex实现动态控制。
路由入口页面
 <keep-alive :include='includes' :exclude='':max="3">   <router-view></router-view> </keep-alive>
 
  | 
 
其中include代表着要缓存的,exclude代表着非缓存的,max代表最多缓存的个数。
 import {mapGetters} from 'vuex' export default {   computed: {      ...mapGetters(['includes']),    },   methods: {      changeStore() {                this.$store.commit('change', 'tableLists')      }    } }
 
  | 
 
Vuex
const keepalive = {   state: {     includes: ['tableLists']   },   mutations: {     change(state, payload) {       state.includes = payload     },   },   getters: {     includes(state) {       return state.includes     }   } };
  export default keepalive
  | 
 
列表页的部分代码
activated() {      if (this.hasFirst) return   this.queryList() }, beforeRouteEnter(to, from, next) {      window._store.commit('change', ['tableLists']);   next()             }, beforeRouteLeave(to, from, next) {      from.meta.scrollTop = document.documentElement.scrollTop;    if (to.name !== 'table-detail') {          this.$store.commit('change', []);   }   next() }
  | 
 
详情页的部分代码
beforeRouteLeave(to, from, next) {   if (to.name !== 'table-list') {     this.$store.commit('change', []);   }   next() }
  | 
 
路由页面
因为includes没有在路由里面定义 keepalive,所以上面的scrollBehavior这个方法当使用合成事件跳转的时候,需要做额外的处理
scrollBehavior(to, from, savedPosition) {   if (savedPosition) {     return new Promise((resolve) => {       setTimeout(() => {         resolve(savedPosition)       }, 20)     })   } else {     const ary = ['Invest', 'Store'];            if (ary.includes(from.name)) {       
 
 
 
        from.meta.scrollTop = document.documentElement.scrollTop;     }     return {x: 0, y: to.meta.scrollTop || 0}
    } }
  | 
 
上面的代码比较琐碎,需要添加到每一个页面,所以在实际项目中大家可添加一个keepalive的mixins,方便大家管理。
使用include和exclude的注意点:
- 每个组件内部添加 {name:xx}
 
- 若将include设置空 ‘ ‘ 每个页面都将会缓存
 
- exclude的优先级高于include 使用exclude后
 
人生中第一次发布文章,希望大佬们多多指教,
参考文档
keep-alive
vueRouterIssues
scrollBehavior
导航守卫