注意:这里说的是返回页面滚动位置状态保持,不是简单的keep-alive实现的页面缓存。
应用场景:
A页面为首页,B页面也为列表页面,C页面为B页面的某个列表项详情页面:
A->B->C:A页面进入B页面,滚动到某个列表项 list-item-x ,点击列表项进入页面C。
C->B->A:对于返回的操作,C页面返回B页面,B页面应该保持在 list-item-x ,B页面返回A页面(如果A页面有滚动也应该有滚动保持)。
需求分析:
1.使用keep-alive?
keep-alive只能使当前访问页面缓存,再下次再次进入的时候直接访问之前缓存的页面,不会再有数据请求更新。这样就会出现这种情况,即加入A、B、C三个页面都被缓存,那么在进行A->B->C访问之后,无论是返回还是再访问,那么三个页面都是直接访问缓存页面了,这个就比较尴尬了。
虽然这个问题可以通过beforeEach对指定路由跳转进行动态配置,来确定页面是否需要缓存。也可以结合Keep-alive的 activated deactivated 生命周期的用法来控制当前访问缓存内容还是重新请求,但无论哪种方法,都是比较麻烦的。
而且,keep-alive组件只能缓存页面数据渲染,并不能保存页面滚动,也就是说页面返回仍会返回到顶部展示。如果要做到滚动保持,仍需特殊处理。经验证,hash模式下使用better-scroll插件来保持滚动状态会比较好些。
鉴于以上 keep-alive 的表现,最终还是决定采用 popstate beforeEach 及better-scroll插件来组合实现页面滚动状态保持。
2. 使用better-scroll实现,需要考虑这些问题:
1)返回的监听:vue-router是无法直接判断页面时返回还是访问的,所以如果想做到只在页面返回时对页面滚动做处理,那么就需要用到 popstate 来监听页面返回了;
2)滚动距离的记录:可以使用Better-scroll来记录元素滚动的距离,然后再下次页面加载的时候,如果是页面返回且有页面滚动距离,那么就要做自动滚动处理了;
3)页面滚动的重置:对于页面中有tab切换,以及返回上级页面,都是需要重置当前页面的滚动及返回访问这些标记的,以确保tab切换之后或者返回上一页再次进入当前页面不会保留之前的滚动。这个需要把握好页面返回、滚动距离等相关标记变量赋值、重置的时机;
4)多页面引用:可以考虑封装一个mixin来给所有需要滚动保持的页面使用。
解决方案:
1)使用 popstate 监听返回页面,结合 beforeEach 对返回页面进行标记,如此就能知道页面访问是不是返回了。
2)使用better-scroll记录滚动元素的滚动距离,然后在 beforeRouteLeave 中保存到路由元中。
3)在 created 生命周期方法中判断是否是返回访问,如果是则保存路由元中保存的滚动距离到当前页面变量中。
4)在better-scroll中实现自动滚动,并重置页面路由的相关滚动返回标记变量。
5)使用混入(mixin)对滚动相关业务逻辑进行封装,这样可以给所有需要滚动的页面进行使用(需要考虑所有情况的兼容性)。
大致流程图如下:
如上,关键的几个逻辑分别标了序号,并在左下角做了简单说明,可对照理解。
关键代码:
scrollMixin.js
1 /** 2 * 作者:[email protected] 3 * 时间:2020-07-06 4 * 描述:页面状态保持 5 * isReturn:非常重要!!!保持页面状态所有操作的必要前提(返回路由元获取,beforeEach中重置) 6 * scrollTop:页面滚动距离(页面返回路由元获取,页面滚动获取||页面初始化中重置) 7 * flag_index:tab默认选中索引(页面返回路由元获取,非返回不获取) 8 * isReturnScroll:返回访问需要保持滚动位置(自动滚动后会重置路由元中的相关变量) 9 */ 10 import Bscroll from 'better-scroll' 11 export default function() { 12 return { 13 data() { 14 return { 15 flag_index: 0, //tab当前索引 16 scroll: '', //better-scroll示例对象 17 scrollTop: 0, //页面(指定元素)距离顶部滚动距离 18 isReturnScroll:''//标记页面返回访问需要保持滚动状态 19 } 20 }, 21 created() { 22 //页面加载,获取页面返回、滚动相关标记 23 this.autoScroll(0) 24 }, 25 beforeRouteLeave(to, from, next) { 26 //组件失活,保存页面滚动距离、tab索引到路由配置中 27 this.autoScroll(2) 28 next() 29 }, 30 methods: { 31 autoScroll(type) { 32 let returnData = ''; 33 //记录页面指定元素滚动位置 34 let routeName = this.$route.path; 35 this.$router.options.routes.map((item, index) => { 36 if(item.path == routeName) { 37 //获取页面返回标记 38 let isReturn = item.meta.isReturn || false 39 if(type == 0 && isReturn) { 40 //1.页面创建,返回路由配置对象给到页面 41 //tab索引、滚动距离都必须要在有返回标记的情况下才取用) 42 this.flag_index = item.meta.flag_index || 0 43 this.scrollTop = item.meta.scrollTop || 0 44 this.isReturnScroll=true 45 } else if(type == 2) { 46 //2.组件失活,保存页面滚动距离、tab索引到路由配置中 47 if(item.meta) { 48 item.meta.scrollTop = this.scrollTop 49 } else { 50 item['meta'] = { 51 'scrollTop': this.scrollTop 52 } 53 } 54 if(this.flag_index > 0) item.meta['flag_index'] = this.flag_index 55 } else { 56 //非返回访问,重置路由配置 57 item.meta = {} 58 } 59 } 60 }) 61 if(returnData) return returnData 62 }, 63 /** 64 * 列表滚动事件处理 65 * @param {Object} list 66 */ 67 doScroll(pullingDownRefresh) { 68 var self = this 69 if(!self.scroll) { 70 self.scroll = new Bscroll(self.$refs.wrapper, { 71 click: true, 72 startY: self.isReturnScroll?self.scrollTop:0,//自动滚动处理 73 // 下拉刷新 74 pullDownRefresh: { 75 // 下拉距离超过30px触发pullingDown事件 76 threshold: 30, // 回弹停留在距离顶部20px的位置 77 stop: 0, 78 } 79 }) 80 81 //下拉刷新 82 self.scroll.on('pullingDown', () => { 83 if(pullingDownRefresh) pullingDownRefresh() 84 setTimeout(() => { 85 // 事情做完,需要调用此方法告诉 better-scroll 数据已加载,否则下拉事件只会执行一次 86 self.scroll.finishPullDown() 87 }, 1000) 88 }) 89 90 //保存滚动位置 91 self.scroll.on('scrollEnd', (poy) => { 92 self.scrollTop = poy && poy.y 93 }) 94 } else { 95 self.scroll.refresh() 96 self.scroll.scrollTo(0, 0) //必须添加这个,不然两个tab切换的时候滚动位置会相互影响 97 } 98 } 99 } 100 } 101 }
如上,doScroll函数主要是处理better-scroll的初始化、自动滚动、下拉刷新、上拉加载、滚动监听等。autoScroll函数则主要封装页面返回自动滚动相关标记变量的获取、重置,以及相关业务逻辑处理(包含的情况有点多,需要好好梳理)。
router.js
1 //监听返回处理 2 let returnUrl = ''//返回页面的路径 3 window.addEventListener("popstate", () => { 4 //当前页面触发,location则是要返回的目标页面 5 returnUrl = location.hash 6 }, false); 7 8 router.beforeEach((to, from, next) => { 9 document.title = "9分享兑" 10 router.to = to 11 router.from = from 12 13 //标记当前页面访问是历史记录返回 14 router.options.routes.map((item, index) => { 15 if(!item.meta) item['meta'] = {} 16 //判断当前页面是否是返回访问 17 if(returnUrl && returnUrl.indexOf(item.path) > -1) { 18 item.meta.isReturn = true 19 } else { 20 item.meta.isReturn = false 21 } 22 }) 23 console.log(router.options.routes[4]) 24 next() 25 })
如上,popstate监听页面返回,标记返回页面的路径(点击返回即触发,可以真实的记录每次要返回页面的路径)。在beforeEach中根据返回监听获取的页面相关路径来对返回页面的路由进行路由元配置,添加页面返回标记(必须要用路由元,只有配置到相应页面中,才能对指定页面进行页面状态保持处理)。
页面引用:
<template> <div class="container h100"> <tab class="tabView orderTab border_b" default-color="#999999" :animate="false" active-color="#f51406"> <tab-item class="f15" :selected="flag_index==index" @on-item-click="tabChange(index)" v-if="flag_list.length>0" v-for="(item,index) in flag_list" :key="index">{{item}}tab-item> tab> <div id="taskList" v-if="list&&list.length>0" class="w100 f13" ref="wrapper"> <div class="content"> ...... div> div> ...... div> template> <script> ...... import Mixin from '@assets/js/scrollMinin'; let mixin = new Mixin() export default { name: "myOrder", mixins: [mixin], ...... methods: { ...... /** * 初始化页面 */ initPage() { this.isEmpty = '' this.list = '' this.scroll = '' this.scrollTop=0 this.searchUserOrderList() }, /** * 查询订单列表 */ searchUserOrderList() { var self = this http.searchUserOrderList(self.orderListParams).then(data => { ...... self.$nextTick(() => { self.doScroll(self.initPage)//下拉刷新需要调用页面初始化函数 }) ...... }) }, ...... } } script>
如上,在页面中调用,要处理好better-scroll容器的展示,如果需要下拉刷新,则须封装一个页面初始化函数传到mixin中。
注意事项:
- 本例将better-scroll的相关使用以及页面状态保持相关业务逻辑封装在一个文件中,采用混入的方式进行调用;
- 需要保持状态相关的变量及函数要在mixin文件中进行声明,混入页面后可以直接调用;
- 除了滚动位置保持,示例代码中还有tab选择保持(当前页面tab切换,需要注意tab切换时重置页面滚动位置变量;
- 声明了组合变量 isReturnScroll ,如有需要,可以在页面渲染的时候进行一些干预(如滚动容器中有轮播和数据列表,为不同接口异步获取,如做滚动保持,那么可能会出现比较明显的闪动切换,那么就可以把轮播数据的获取写在列表数据获取之后,虽然还是异步,但基于js的执行顺序,可以有效的降低自动滚动切换时的闪动)
后记:
如果要做tab选择状态保持,需要控制好tab索引变量,毕竟是采用混入(mixin)方式引用,所有引用页面势必要依赖混入变量(tab选择索引)。tab选择索引的混入变量默认值为0,因此需要在各引用页面中处理好tab索引的相关展示以及接口请求。