使用 keep-alive 的 include 属性实现 Vue 页面缓存
众所周知,Vue 中的 keep-alive 可以对组件进行缓存,搭配上 vue-router 的 <router-view>
则可以实现页面缓存。
但网上大多数的方案都是采用在 router 的 meta 属性里增加一个 keepAlive 字段,然后在父组件或者根组件中,根据 keepAlive 字段的状态使用 <keep-alive>
标签,实现对 <router-view>
的缓存,如下:
1 | <keep-alive> |
如果要对页面动态控制是否需要缓存,则是在 beforeRouteLeave()
里去控制 keepAlive 的状态。
这个方法看似简单,但问题挺多,网上的解决方案似乎也不太理想,我甚至连尝试都懒得去尝试。
因为这个方案为了解决一个问题,反而创造出了一堆问题,为了解决这一堆问题,又引入了各种“奇思妙想”、“剑走偏锋”的骚操作,光是看大家的代码就让我头大。
在思考并搜索还有什么更好解决方案的时候,我无意翻看到 Vue 的官方文档,在 keep-alive 的介绍里看到, 2.1.0 里新增了 include 和 exclude 这两个属性,这似乎给我了一点思路。
于是带着这两个关键词,重新去百度里搜寻了一番,果然,已经有现成的解决方案了。
实现思路
这个解决方案思路其实很清晰,因为 include 属性支持传入字符串、正则和数组,利用 vuex 全局去管理 include 里的数据,就可以达到动态管理缓存。
比起开篇介绍的那个方案,这个方案从始至终都没有销毁 <router-view>
,从而规避了很多无形的坑。加上 include 本身又是官方提供的属性,跟着官方走,准没错!
实现代码
老罗说的好:少废话,先看东西。
首先 include 属性里存放的是组件的 name ,也就是说,我们的页面组件必须都先设置上 name ,注意了,这个 name 并不是 router 里的 name ,而是组件的 name 。
接着,因为 include 的数据是通过 vuex 动态管理的,所以需要定义一个 store ,代码如下:
1 | const state = { |
在 mutations 里定义了三个对 list 状态更改的事件,分别是 add
、remove
、clean
,随后我们在父组件或者根组件中就可以这样使用了。
1 | <keep-alive :include="$store.state.keepAlive.list"> |
准备工作做好后,那什么时候去控制 include 里的数据呢?那就是在页面进入和离开的时候去控制就行,这里就需要用到 beforeRouteEnter()
和 beforeRouteLeave()
这两个钩子函数。
我们假设这样一个场景,有这样两个页面,一个商品列表页(A),一个商品详情页(B),当从 A 页面跳转到 B 页面的时候,希望把 A 页面缓存上,这样在 B 页面做 $router.go(-1)
这种返回操作的时候,可以继续浏览 A 页面的内容。代码如下:
1 | // A 页面 |
这里有一点需要注意,当离开 A 页面前,需要判断去往的页面是否为 B 页面,也就是这句 if (['detail'].indexOf(to.name) < 0)
代码(这里的 detail
是去往页面 router 里的 name ,并非组件的 name),如果去往的页面不是 B 页面,则清除缓存,比如从 A 页面返回了更上级的页面,如果不清除,下次再进来的时候,会直接调取缓存,而不是全新打开。
是不是很简单?思路是不是也特别清晰?先别着急,我们来踩踩坑。
踩坑
缓存无法清除
以上面举的例子,想要清除 A 页面的缓存,必须从 A 页面进行操作,比如从 A 页面返回到更上级的 C 页面。
但在实际业务中,页面之间的联系并非是一条直线的。比如从 A 页面进入 B 页面, B 页面有个功能按钮是可以直接进入 C 页面的,这时候再从 C 页面进入 A 页面, A 页面的缓存是还存在的,导致打开还是上次缓存的内容,而不是全新的 A 页面。
这时候就需要用到 $store.commit('keepAlive/clean')
了,因为涉及到具体业务逻辑,所以在什么时候调用 clean 方法需要具体页面具体分析。我的原则就是在顶级,或者次顶级页面上,做缓存清空处理,比如例子中的 C 页面,或者是一般项目的首页。
页面刷新后缓存失效
关于 Vue 刷新的问题,我在《Vue中刷新当前页的几种方式及优劣分析》已经有提到过。
其中方案三的刷新,无法和 keep-alive 共存,所以在需要缓存的相关页面里,建议使用方案二,或者使用方案四,手动进行数据更新。
如何更新缓存
有这么一种情况,从 A 页面进入 B 页面,在 B 页面做了一些操作后,返回 A 页面,这时候 A 页面部分数据要进行更新。
最常见的就是订单列表页,从订单列表页进入订单详情页,在订单详情页里做了一些操作,比如关闭该订单,这时改变了订单的状态,当返回的时候,订单列表页虽然被缓存了,但列表里的信息要进行更新。
我自己想到的方案是,在 B 页面离开前,往去往页面的 meta 里添加一个特定字段,例如 to.meta.returnRefresh
,至于这个字段什么时候要添加,我们可以自己控制。然后在订单列表页的 activated()
钩子里处理即可。
1 | // 订单详情页 |
我的这个方案没什么大问题,就是在体验上有点欠缺。因为 A 页面的更新,是当 A 页面被激活后才会进行,能明显看到返回 A 页面后,数据才进行更新,整个过程用户是有感的。
于是我开始在网上搜寻相关解决方案,同时在用 Vue 开发者工具操作的时候发现一个细节:
因为 A 页面被缓存了,所以实际上 A 页面和 B 页面这两个 <router-view>
是并存的,只是其中一个被隐藏了。既然这两个组件是并存的,我开始有方向了,搜索一圈之后,找到了解决方案。
简单来说,就是兄弟组件之间的通信,父子组件的通信我们比较了解,但兄弟平级组件之间的通信,和父子组件不一样,他们需要借助事件总线,因为 $on()
和 $emit()
的事件必须是在一个公共的实例上才能触发,那我们可以新建一个 Vue 实例当作事件总线,达到可以不管组件之间的父子关系,都能通过这个实例通信的目的。
这里我偷懒了,直接把现有 Vue 实例当做事件总线,并将它绑定到 Vue 原型链上,方便后续使用。
1 | // main.js |
准备好后,我们来看下如何在订单详情页通知订单列表页进行数据更新。
1 | // 订单详情页 |
我们在订单详情页里任何时候都可以通过 this.$eventBus.$emit('refreshOrderList')
去通知订单列表页更新数据,这样数据的更新对用户来说是无感的,用户返回订单列表页的时候,数据是已经更新好了,对用户体验上有明显的提升。
避免意外情况,在订单列表页被销毁前,手动销毁下监听的事件,这样就万无一失了。
总结
其实通篇的解决方案,网上都能找到类似的影子,如何将它们合理的使用在项目或产品中,这才是我们需要多去思考的。
其次我似乎没有遇到从 include 列表移除组件,组件没有被销毁的问题,可能 Vue 已经修复了这个 bug 吧。