记一次vue 的keep-alive踩坑之路
这个坑,从项目开始的时候就一直存在,在前期的时候还没有影响到正常的流程,加之项目比较紧,就一直没有花太多时间去管它。 可是在最近的业务中,这个问题已经影响到了正常的流程,所以这周一直在寻找这个问题的解决方案。在经历了各种论坛和各种贴子之后,找到了我目前能找到的最合适的解决方法,于是写下了这篇文章。
背景
用vue+cordova 做一个hybrid App,由于业务涉及的输入数据比较多,可能一个表单会包含多个子页面,这就要求在录入数据的时候,进入子页面之前需要将当前页的数据缓存,而在子页面返回的时候,子页面要销毁。也就是说,app正常前进的时候,页面全部缓,返回的时候之前的页面不需要刷新数据,直接读取缓存数据。
具体场景和这位仁兄一致: 另辟蹊径:vue单页面,多路由,前进刷新,后退不刷新
为了找到一个合适的解决方案,我将这个场景进行了尽可能的简化,做成了一个DEMO,用来模拟这个场景。
Demo是用Vue-cli生成的,加上了简单的几个文件
项目结构如下图:
项目结构图
其中,child目录用来模拟子路由的,first/second是一级路由
具体路由文件如下:
router/index.js
最开始,什么都不处理,只要上页面上加上<router-link/> 是可以直接跳转的,当然也不会缓存任何组件。
给子路由加上缓存
我的最终目的给项目中所有的模块进行单独缓存,而一级页面不缓存。所以用child来模拟一个模块,first.vue 和 second.vue来模拟一级页面
每个模块(child)都有自己的入口,如child/index.vue,内容如下:
child/index.vue
这样可以将child下面所有的子路由进行缓存,可以用vue-devtool工具进行查看是否能够成功缓存
vue-devtool
可以看到,child下面的list以及detail组件已经缓存成功了。
到这里,已经实现了子路由的成功缓存。
但是这样会有一个问题:在页面返回的时候,所以被缓存的组件,会一直被缓存,再次进入页面的时候,组件并不会自动刷新,所以造成从list 点击不同的详情页的时候,进入的都是同一个页面。
在vue-devtool中看到的现象如下图:
当然,这种情况下也是有方法进行数据刷新的。因为所有被缓存的组件,再次进入的时候,会触发其生命周期的activated方法(https://cn.vuejs.org/v2/api/#keep-alive),可以在此进行强制数据刷新。
显然,这种方法不是我想要的。
在返回之前销毁当前实例
上面那种方法的问题在于:页面返回的时候,后面的组件(实例)还被缓存着,所以vue并不会重新去刷新它们。那么,如果在页面返回之前将自己销毁掉,vue也就找不到了,问题不就解决了么
正好,我项目用的ui框架是vonic https://github.com/wangdahoo/vonic/ ,在学习大神的源码的时候,发现了这样的一段代码,
这段代码的作用有两点:1、设置页面切换的动画(前进=》'forward',返回=>'back' );2、设置页面返回后的page position,也就是页面滚动的位置。
实现原理大致如下:
用sessionStorage来存储app打开过的路由,每当在路由跳转之前,先判断将要去的路由toRoute是不是在sessionStorage里面存在:如果存在,那么页面就是返回,将下一次路由切换的动画换成back,并改变sessionStorage中的history=false;如果不存在,那就是前进打开新的页面,同时将下一次路由切换的动画改成forward,同时改变sessionStorage中的history=true,并记录当前页的scrollTop。
其实vue-router本身的路由和浏览器的history对象一样,对应用程序来说,是不能直接操作的,这里作者用sessionStorage来模拟一个history,从而实现某些功能,这种方法是值得点赞的。
到这里,我们已经可以知道应用曾经打开过哪些页面,那我们在返回的时候是不是可以将某些页面销毁呢?答案是肯定的。
我们要做的是,在页面返回之前,也就是路由切换之前,要获取需要销毁的实例对象。
上图中有一个beforeEach,是vue-router提供的一个全局的钩子函数,它可以监听到每一次路由的切换,但却获取不到任何vue的实例对象。
在看了几遍vue-router的文档之后https://router.vuejs.org/zh-cn/advanced/navigation-guards.html ,我找到了一个组件内的钩子,也就是官方文档提到的导航守卫,如下图所示:
其中,我们要用到的是这个beforeRouteLeave,在将要离开组件对应路由的时候会触发这个钩子。而最重要的是,这个钩子可以获取到当前的组件实例this,所以我们可以在这个时候调用this.$destory()来销毁当前实例,就能完美解决上面提到的问题。
而这个钩子只能在组件内使用,也就意味着我每个需要缓存的组件都要调用这个钩子,难道每个组件里面都去写一遍这个钩子函数吗?
当然不用。
vue提供了全局的mixin方法,可以帮你在所有的实例里面加上这个钩子。
由于我的demo中没有用到vonic,所以我只能参照大神的源码,写了一段,具体如下:
main.js
这样,便可以实现,前进的时候缓存组件,返回的时候不刷新,并将最后一个页面销毁。
理论上,这个方案挺完美的。