> ## **keep-alive组件实战应用** ### **页面缓存** 在Vue构建的单页面应用(SPA)中,路由模块一般使用vue-router。vue-router不保存被切换组件的状态,它进行push或者replace时,旧组件会被销毁,而新组件会被新建,走一遍完整的生命周期。 但有时候,我们有一些需求,比如跳转到详情页面时,需要保持列表页的滚动条的高度,等返回的时候依然在这个位置,这样可以提高用户体验。在Vue中,对于这种“页面缓存”的需求,我们可以使用keep-alive组件来解决这个需求。 ### **使用方式** keep-alive是个抽象组件(或称为功能型组件),实际上不会被渲染在DOM树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次再渲染的时候,还会保持其中的所有状态,并且会触发activated钩子函数。因为缓存的需要通常出现在页面切换时,所以常与router-view一起出现: ~~~ <keep-alive> <router-view/> </keep-alive> ~~~ 如此一来,每一个在router-view中渲染的组件,都会被缓存起来。 ### **生命周期** ~~~ <div id="app"> <input type="button" value="切换到第1个组件" @click="tabComponent(1)"> <input type="button" value="切换到第2个组件" @click="tabComponent(2)"> <input type="button" value="切换到第3个组件" @click="tabComponent(3)"> <keep-alive> <component :is="current"></component> </keep-alive> </div> <script> // 第一个组件 const custom1 = Vue.component('custom1', { template: `<div @click="changeBg">我是第1个组件</div>`, methods: { changeBg(ev) { ev.target.style.background = 'orange' } } }) // 第二个组件 const custom2 = Vue.component('custom2', { template: `<div>我是第2个组件</div>`, created: function() { // 初始化执行一次,不再执行了 console.log('created') }, activated: function() { // 如果用keep-alive包裹后,需要每次进来完成一些数据的操作,请在这个钩子里 console.log('activated') }, deactivated: function() { // 离开触发 console.log('deactivated') } }) // 第三个组件 const custom3 = Vue.component('custom3', { template: `<div>我是第3个组件</div>` }) new Vue({ el: '#app', data: { current: custom1 }, methods: { tabComponent(index) { if (index === 1) { this.current = custom1 } else if(index ===2) { this.current = custom2 } else { this.current = custom3 } } } }) </script> ~~~ ### **实战案例一** 第一个需求:从列表页通过路由跳转到详情页,当从详情页返回到列表页的时候,要能够保存列表页滑动的高度 #### **第一步:** ~~~ <keep-alive> <router-view/> </keep-alive> ~~~ #### **第二步:** ~~~ export default { data () { return { dataList: [], scroll: 0 // 第一步:初始值 } }, created () { // 这里可以是ajax请求 }, mounted () { window.addEventListener('scroll', this.handleScroll) // 第二步:DOM挂载后添加事件 }, // 第三步:获取顶部距离 methods: { handleScroll () { this.scroll = document.documentElement && document.documentElement.scrollTop console.log(this.scroll) } }, // 第四步:activated 为keep-alive加载时调用 activated () { if (this.scroll > 0) { window.scrollTo(0, this.scroll) this.scroll = 0 window.addEventListener('scroll', this.handleScroll) } }, // 第五步:组件退出时关闭事件 防止其他页面出现问题 deactivated () { window.removeEventListener('scroll', this.handleScroll); } } ~~~ #### **第三步:演示** 当你从列表跳到详情以后,然后再从详情返回到列表,就可以返回到离开列表时候的位置了 ![](https://i.vgy.me/Uwbjne.gif) > 问题:既然我们用keep-alive包裹了所有的组件,那么出现一个问题,详情页的数据始终是第一次请求的,因为被缓存起来了,该怎么办呢? #### **问题演示** ![](https://i.vgy.me/5LFn8S.gif) #### **解决方案:** 当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> created-> activated,退出时触发deactivated。 当再次进入(前进或者后退)时,只触发activated。 这就带来一个问题,之前在项目中使用created在页面加载时获取数据,使用后方法不再生效。 根据上面的解释,将created替换为activated即可: ~~~ export default { data () { return { detailData: {} } }, created () { // Axios.get('https://api.it120.cc/small4/shop/goods/detail?id=' + this.$route.query.id).then(res => { // let { data } = res.data // this.detailData = data // }) }, activated () { Axios.get('https://api.it120.cc/small4/shop/goods/detail?id=' + this.$route.query.id).then(res => { let { data } = res.data this.detailData = data }) } } ~~~ #### **解决后的效果演示** ![](https://i.vgy.me/4p9pfB.gif) 详情页缓存的数据和新数据有闪烁问题,请大家自行解决,不算什么问题我觉得 > 问题:如果说我们的详情页很高的话,会出现一个问题,请看图片演示 #### **问题演示** ![](https://i.vgy.me/jD7kKV.gif) 当我们从列表跳转到详情页可以看到我们的商品图片没有显示出来,其实并不是没有显示,而是我们想请页的滚动条有了高度导致的,该怎么解决呢? #### **解决方案** ~~~ router.afterEach((to, from, next) => { window.scrollTo(0, 0) }) ~~~ 重置scroll的坐标,上面这段代码确实解决了, > 问题:但是带来了新的问题,我返回商品列表页面也返回到了顶部,好吧!这也不是我想要的效果,肯定需要记住用户浏览时的位置啊,拆东墙补西墙的感觉。 #### **解决方案** 比较简单的方法,就是在详情页加上如上代码 ~~~ methods: { scrollToTop () { window.scrollTo(0, 0) } }, activated () { Axios.get('https://api.it120.cc/small4/shop/goods/detail?id=' + this.$route.query.id).then(res => { let { data } = res.data this.detailData = data }) this.scrollToTop() } ~~~ 每次进入详情页让页面滚动到最顶部,问题得以解决! > 你还可以使用router路由进行控制,使用router提供的`scrollBehavior`方法,官网自行解决吧,应该没什么难度!!!,有难度告诉我,我来写一个简单易懂的示例!!!O(∩_∩)O哈哈~ ***** ### **实战案例二** ``` <keep-alive> <router-view /> </keep-alive> ``` 如此一来,每一个在router-view中渲染的组件,都会被缓存起来。 如果只想渲染某一些页面/组件,可以使用keep-alive组件的include/exclude属性。include属性表示要缓存的组件名(即组件定义时的name属性),接收的类型为string、RegExp或string数组;exclude属性有着相反的作用,匹配到的组件不会被缓存。假如可能出现在同一router-view的N个页面中,我只想缓存列表页和详情页,那么可以这样写: ``` <keep-alive :include="['ListView', 'DetailView']"> <router-view /> </keep-alive> ``` 上面include的写法不是常用的,因为它固定了哪几个页面缓存或不缓存,假如有下面这个场景: * 现有页面:首页(A)、列表页(B)、详情页(C),一般可以从:A->B->C; * B到C再返回B时,B要保持列表滚动的距离; * B返回A再进入B时,B不需要保持状态,是全新的。 很明显,这个例子中,B是“条件缓存”的,C->B时保持缓存,A->B时放弃缓存。其实解决方案也不难,只需要将B动态地从include数组中增加/删除就行了。 #### **实现条件缓存:全局的include数组** ##### **1. 在Vuex中定义一个全局的缓存数组,待传给include:** ~~~ /** *Created by SmallFour on 2019/8/17/10:21 */ import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { keepAliveComponents: ['List'] // 缓存数组;默认缓存list }, mutations: { // 缓存 keepAlive (state, component) { console.log(component) !state.keepAliveComponents.includes(component) && state.keepAliveComponents.push(component) }, // 不缓存 noKeepAlive (state, component) { const index = state.keepAliveComponents.indexOf(component) index !== -1 && state.keepAliveComponents.splice(index, 1) } } }) export default store ~~~ ##### **2. 在父页面中定义keep-alive,并传入全局的缓存数组:** ~~~ <template> <div id="app"> <keep-alive :include="keepAliveComponents"> <router-view/> </keep-alive> </div> </template> <script> export default { name: 'App', computed: { keepAliveComponents () { return this.$store.state.keepAliveComponents } } } </script> ~~~ ##### **3. 缓存:在路由配置页中,约定使用meta属性keepAlive,值为true表示组件需要缓存。在全局路由钩子beforeEach中对该属性进行处理,这样一来,每次进入该组件,都进行缓存:** ~~~ const router = new Router({ mode: 'history', routes: [ { path: '/', name: 'Index', component: Index, meta: { title: '首页' } }, { path: '/list', name: 'List', component: List }, { path: '/detail/:id?', name: 'Detail', component: Detail, meta: { title: '详情页', keepAlive: true // 这里指定detail组件需要被缓存 } } ] }) router.beforeEach((to, from, next) => { // 在路由全局钩子beforeEach中,根据keepAlive属性,统一设置页面的缓存性 // 作用是每次进入该组件,就将它缓存 if (to.meta.keepAlive) { next() router.app.$store.commit('keepAlive', to.name) } next() }) ~~~ ##### **4. 取消缓存的时机:对缓存组件使用路由的组件层钩子beforeRouteLeave。因为list->index->list时不需要缓存list,所以可以认为:当B的下一个页面不是detail时取消list的缓存,那么下次进入list组件时list就是全新的:** ~~~ beforeRouteLeave (to, from, next) { // 如果下一个页面不是详情页(C),则取消列表页(B)的缓存 if (to.name !== 'Detail') { this.$store.commit('noKeepAlive', from.name) } next() } ~~~ ##### **5. 效果演示** 因为list的条件缓存,是list自己的职责,所以最好把该业务逻辑写在B的内部,而不是index中,这样不至于让组件之间的跳转关系变得混乱。 #### **实现条件缓存:另一种方式** **没有实测,逻辑上没问题** 在父组件中,使用两个router-view并进行条件渲染: ``` // App.vue <div id="app"> <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view> </div> ```