需求如图,首页日常任务模块—>日常任务列表(父组件)–>任务列表(子组件)–>详情页(孙组件)。
就是从首页进入功能列表页父组件,滚动一段距离点击自立项目
进入到任务子组件,再滚动一段距离点击第一条带日期的那一项
进入任务详情,然后返回时要返回到刚才滚动的位置,而不是从列表第一条开始。
项目使用的vue
框架和vant
组件,我按照vue
官方文档里介绍说把<router-view>
用<keep-alive>
包裹起来就可以实现,各组件内也定义了name
属性,然而并没有卵用,每次返回都是从第一条显示。网上搜了下大概有两个原因:
1、<router-view>
嵌套在层级不同的<router-view>
中切换会出现缓存数据失效。
2、<keep-alive>
只对直属的子组件有效,多个共用组件导致的<keep-alive>
缓存失效。
解决方法:
1、既然是多个router-view
嵌套并且共用的情况下造成的,那么如果只存在一个router-view
,也就是只需要app.vue
作为框架内所有页面的容器,就不会有这个问题。然后将router
转换一下,全部转换成一级路由。
2、不转换路由层级,使用scrollIntoView()
或者获取父组件的scrollTop
。
Element
接口的scrollIntoView()
方法会滚动元素的父容器,使被调用scrollIntoView()
的元素对用户可见。具体请参考
Element.scrollTop
属性可以获取或设置一个元素的内容垂直滚动的像素数。一个元素的scrollTop
值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的scrollTop
值为0
。scrollTop
我在项目中使用了第二种解决方案,主要原因:
1、首先我觉得带层级的路由更清晰些。
2、在缓存的路由中引用封装的组件,缓存会造成其它列表项数据不正确,要作额外理。
3、因为scrollIntoView()
只能设置滚动位置为start/center/end/nearest
,滚动位置并不精确,体验不是太好,而且兼容性没有scrollTop
好,所以选择了 Element.scrollTop
。
注意:这里有个坑,scrollTop
一定要获取父组件的(或最外层router-view),即使是在子组件也要获取父组件的scrollTop
,因为所有的内容都是包含在父组件里的,其实就是router-view
。我一开始是获取的vant
列表的scrollTop
,不管怎么滚动scrollTop
一直为0,纠结了不少时间。
具体操作步骤:
1、首先在父组件内用ref
属性获取到最外层元素
// 父组件
<template>
<div class="dailyTaskMain commonStyle" ref="dailyTaskMain">
// 封装的通用组件,里面包含了vant列表,点击列表中的自立项目时会触发getScroll函数
// 这里使用了v-show,如果是v-if要把滚动距离保存到vuex里
<div v-show="isShowHeadAndTabBar">
<CommonParentPage @getScroll="getScroll"></CommonParentPage>
</div>
<!-- 显示子路由 -->
<router-view></router-view>
</div>
</template>
<script>
export default {
data () {
return {
scroll: ''
}
},
watch: {
// 监测路由路径变化,返回父路由时,刷新滚动距离
$route (now, old) {
if (old.name === 'dailyChildTask') {
this.$nextTick(() => {
this.$refs.dailyTaskMain.scrollTop = this.scroll
})
}
}
},
methods: {
// 获取滚动距离
getScroll () {
this.scroll = this.$refs.dailyTaskMain.scrollTop
}
}
}
</script>
这样就拿到了父组件的内容的滚动距离,从子组件返回时,将滚动距离赋值给父组件最外层元素。完美解决!
2、在子组件内获取内容的滚动距离
此时,父组件的内容被隐藏了,只剩下子组件的内容。注意,子组件的内容也是包含在父组件内的,所以我们要想获取子组件内容的滚动距离,首先还是得拿到父组件的最外层元素。
vue
提供了一个属性$parent
,从字面意思就可以猜出,它是用来获取父组件信息的。
在子组件我们使用this.$parent.$el
就可以得到父元素的最外层元素。
// 子组件
<template>
<div class="dailyChildTask commonAddBtn">
<div v-show="isShow">
// 封装的通用组件
<CommonChildPage @getParentScroll="getParentScroll"></CommonChildPage>
</div>
</div>
</template>
<script>
export default{
data(){
return{
scroll: ''
}
},
watch: {
// 监测路由路径变化,返回父路由时,刷新滚动距离
$route (now, old) {
if ((old.name === 'dynamicForm')) {
this.$parent.$el.scrollTop = this.scroll
}
}
},
methods:{
// 获取父组件滚动距离(点击列表项时封装的子组件触发$emit从而触发getParentScroll,获取父组件内容的滚动距离)
getParentScroll () {
this.scroll = this.$parent.$el.scrollTop
}
}
}
</script>
注意:
v-show
的妙用项目中父、子组件切换时我使用的是
v-show
,因为v-show
的特性(隐藏元素而不是销毁,相当于缓存了),子组件返回父组件、详情页返回子组件时不会重新刷新页面。所以this.scroll
的值在上述情形下不会重置为0。如果使用
v-if
(销毁元素),这时页面会重新刷新,this.scroll
的值会重新初始化,所以返回时父元素的scrollTop
始终为0。综上所述,使用v-if
时不能用$emit
方法来直接获取scrollTop
,而是将scrollTop
保存到vuex
中,返回时从vuex
获取scrollTop
。当然使用
v-show
也可以使用vuex
来保存scrollTop
。这时就不需要$emit
方法和getScroll
等监听方法。直接从vuex
里获取scrollTop
的值。
使用v-if
的注意事项
1、使用v-if
页面会重新加载,所以我们要在数据请求成功后立即获取scrollTop
getList(){
API.xxx().then(resp => {
if(resp.httpCode === 200){
this.list = resp.xxx // 拿到列表数据
this.$nextTick(() => {
// 我这里封装了通用组件,在通用组件内获取父组件的scrollTop要用this.$parent.$el
this.$parent.$el.scrollTop = this.$store.state.xxx
})
}
})
}
2、通过第一步我们是能拿到了scrollTop
。但是有个问题,不管页面前进还是后退,都定位到了之前保存的位置。我们期待的效果是页面前进时scrollTop
要为0,即从第一条数据开始。怎么办呢?
我们可以增加一个变量来控制scrollTop
的获取。比如在vuex
定义一个flag
。
在钩子函数里监听(路由全局前置守卫)
router.beforeEach((to, from, next) => {
// 判断是否需要刷新滚动位置
let name = from.name
if (name === 'index') {// 如果是首页,即是页面前进
store.commit('changeFlag', 0)
} else {
store.commit('changeFlag', 1)
}
})
3、改写获取列表方法
// 获取列表
getList(){
API.xxx().then(resp => {
if(resp.httpCode === 200){
this.list = resp.xxx // 拿到列表数据
// 修改获取scrollTop的时机
if(flag){
this.$nextTick(() => {
// 我这里封装了通用组件,在通用组件内获取父组件的scrollTop要用this.$parent.$el
this.$parent.$el.scrollTop = this.$store.state.xxx
})
} else {
this.$parent.$el.scrollTop = 0
}
}
})
}
4、下拉刷新时要将flag
置为0
5、使用v-if
页面会闪一下,因为在重新加载页面