vue keep-alive 路由缓存添加动画之后怎么保存页面滚动位置

先看效果:

打开链接:example-keep-alive.html

或手机扫码查看效果:

动画实现思路

百度一下会有很多实现方式,基本都是利用路由元信息,再结合watch实现动画效果。

以上方式实现动画毫无问题,但是在进行历史记录后退和前进操作时候无法记录滚动条位置,每次都会回到页面最顶部。

解决办法

vue-router提供有一个scrollBehavior方法,可以设置滚动条位置,也可以获取到滚动条位置,本文就是用该方法记录滚动条位置,再结合动画钩子 beforeEnter 模拟页面滚动,最后使用 afterEnter 还原滚动条位置。

看代码

路由跳转记录滚动条位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var router = new VueRouter({
mode: 'hash',
scrollBehavior(to, from, savedPosition) {
// 记录滚动条位置到meta信息,用于路由动画完成之后设置滚动条位置
if (savedPosition) {
to.meta.top = savedPosition.y
return savedPosition
} else {
to.meta.top = 0
return { x: 0, y: 0 }
}
},
routes: [
{
path: '/',
name: 'index',
component: Vue.options.components.Index,
meta: {
index: 1, // 路由层级,用于动画判断是从左还是从右进入
top: 0 // 记录页面滚动位置
}
},
{
path: '/details',
name: 'details',
component: Vue.options.components.Details,
meta: {
index: 2, // 路由层级,用于动画判断是从左还是从右进入
top: 0 // 记录页面滚动位置
}
}
]
})

App 组件监听路由变化实现

模版

1
2
3
4
5
6
7
8
9
<script type="text/x-template" id="app">
<div class="app">
<transition :name="transitionName" @after-enter="afterEnter" @before-enter="beforeEnter">
<keep-alive>
<router-view :key="$route.fullPath" />
</keep-alive>
</transition>
</div>
</script>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Vue.component('App', {
template: '#app',
data: function () {
return {
transitionName: ''
};
},
watch: {
$route(to, from) {
// 动画判断是从左还是从右进入
if (to.meta.index && from.meta.index) {
if (to.meta.index > from.meta.index) {
// 设置动画名称
this.transitionName = 'route-slide-left'
} else {
this.transitionName = 'route-slide-right'
}
}
}
},
methods: {
beforeEnter(el) {
this.$nextTick(() => {
// 动画开始使用定位实现页面滚动位置
const top = this.$route?.meta?.top
if (top) {
el.style.top = -top + 'px'
}
})
},
afterEnter(el) {
const top = this.$route?.meta?.top
this.$nextTick(() => {
// 动画结束之后,定位清除,使用 scrollTop 还原滚动条位置
el.style.top = ''
if (top) {
document.documentElement.scrollTop = top
document.body.scrollTop = top
}
})
}
}
})

CSS 实现动画效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.route-slide-right-enter-active,
.route-slide-right-leave-active,
.route-slide-left-enter-active,
.route-slide-left-leave-active
{
will-change: transform;
position: absolute;
left: 0;
top: 0;
width: 100%;
transition-property: opacity, transform;
transition-duration: .2s;
transition-timing-function: ease;
}

.route-slide-right-enter {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}

.route-slide-right-leave-active {
opacity: 0;
transform: translate3d(100%, 0, 0);
}

.route-slide-left-enter {
opacity: 0;
transform: translate3d(100%, 0, 0);
}

.route-slide-left-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}

页面组件中的代码相对简单,此处不再列出,请看后面完整代码。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue keep-alive 路由缓存添加动画</title>
<style>
body {
margin: 0;
padding: 0;
}
.app {
width: 100%;
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
.index {
background-color: blanchedalmond;
padding: 30px;
width: 100%;
box-sizing: border-box;
}
.details {
background-color: aqua;
padding: 30px;
width: 100%;
box-sizing: border-box;
}
.route-slide-right-enter-active,
.route-slide-right-leave-active,
.route-slide-left-enter-active,
.route-slide-left-leave-active
{
will-change: transform;
position: absolute;
left: 0;
top: 0;
width: 100%;
transition-property: opacity, transform;
transition-duration: .2s;
transition-timing-function: ease;
}

.route-slide-right-enter {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}

.route-slide-right-leave-active {
opacity: 0;
transform: translate3d(100%, 0, 0);
}

.route-slide-left-enter {
opacity: 0;
transform: translate3d(100%, 0, 0);
}

.route-slide-left-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
</style>
</head>
<body>
<section id="container"></section>
<script type="text/x-template" id="app">
<div class="app">
<transition :name="transitionName" @after-enter="afterEnter" @before-enter="beforeEnter">
<keep-alive>
<router-view :key="$route.fullPath" />
</keep-alive>
</transition>
</div>
</script>
<script type="text/x-template" id="index">
<div class="index">
<h3>点击历史记录跳转会保存滚动条位置</h3>
<p v-for="(item,index) in list" :key="index">
<template v-if="index % 8 !== 0">
{{ index }}
</template>
<template v-else>
<p><a href="javascript:;" @click="$router.go(1)">历史记录前往下一页</a></p>
<router-link :to="{ name: 'details' }">跳转前往详情页</router-link>
</template>
</p>
</div>
</script>
<script type="text/x-template" id="details">
<div class="details">
<h3>点击历史记录跳转会保存滚动条位置</h3>
<p v-for="(item,index) in list" :key="index">
<template v-if="index % 8 !== 0">
{{ index }}
</template>
<template v-else>
<p><a href="javascript:;" @click="$router.go(-1)">历史记录返回上一页</a></p>
<p><a href="javascript:;" @click="$router.push({ name: 'index' })">跳转前往首页</a></p>
</template>
</p>
</div>
</script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.5.1/vue-router.min.js"></script>
<script>
var list = (new Array(50)).fill('1')
Vue.component('Index', {
template: '#index',
data: function () {
return {
list: list
}
},
})
Vue.component('Details', {
template: '#details',
data: function () {
return {
list: list
}
},
})
Vue.component('App', {
template: '#app',
data: function () {
return {
transitionName: ''
};
},
watch: {
$route(to, from) {
// 动画判断是从左还是从右进入
if (to.meta.index && from.meta.index) {
if (to.meta.index > from.meta.index) {
// 设置动画名称
this.transitionName = 'route-slide-left'
} else {
this.transitionName = 'route-slide-right'
}
}
}
},
methods: {
beforeEnter(el) {
this.$nextTick(() => {
// 动画开始使用定位实现页面滚动位置
const top = this.$route?.meta?.top
if (top) {
el.style.top = -top + 'px'
}
})
},
afterEnter(el) {
const top = this.$route?.meta?.top
this.$nextTick(() => {
// 动画结束之后,定位清除,使用 scrollTop 还原滚动条位置
el.style.top = ''
if (top) {
document.documentElement.scrollTop = top
document.body.scrollTop = top
}
})
}
}
})

var router = new VueRouter({
mode: 'hash',
scrollBehavior(to, from, savedPosition) {
// 记录滚动条位置到meta信息,用于路由动画完成之后设置滚动条位置
if (savedPosition) {
to.meta.top = savedPosition.y
return savedPosition
} else {
to.meta.top = 0
return { x: 0, y: 0 }
}
},
routes: [
{
path: '/',
name: 'index',
component: Vue.options.components.Index,
meta: {
index: 1, // 路由层级,用于动画判断是从左还是从右进入
top: 0 // 记录页面滚动位置
}
},
{
path: '/details',
name: 'details',
component: Vue.options.components.Details,
meta: {
index: 2, // 路由层级,用于动画判断是从左还是从右进入
top: 0 // 记录页面滚动位置
}
}
]
})
new Vue({
router: router,
render: h => h(Vue.options.components.App)
}).$mount('#container')
</script>
</body>
</html>

问题

以上代码已可以比较完整的实现路由动画和记录滚动条位置了,但是还存在一个问题,如果在两个页面间快速来回点击前进和后退,最终滚动条还是会停在页面顶部,原因是快速点击页面动画的afterEnter未执行,导致scrollBehavior获取到的滚动条位置为0,暂未找到比较好的方法规避此问题。

本文由 linx(544819896@qq.com) 创作,采用 CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本文链接为: https://blog.jijian.link/2022-05-10/vue-keep-alive/

如果您觉得文章不错,可以点击文章中的广告支持一下!