68. 滚动行为与过渡动画
📖 概述
滚动行为和过渡动画是提升单页应用用户体验的重要特性。Vue Router 4.x提供了灵活的滚动行为配置,允许我们在路由切换时控制页面滚动位置;同时,结合Vue的过渡系统,我们可以实现平滑的页面切换动画。本集将深入讲解Vue Router 4.x中滚动行为的配置、过渡动画的实现以及两者的结合使用,帮助你构建更流畅、更吸引人的单页应用。
✨ 核心知识点
1. 滚动行为基础
什么是滚动行为
- 滚动行为是指在路由切换时,页面滚动位置的控制方式
- Vue Router 4.x允许我们自定义路由切换时的滚动行为
- 可以控制滚动到顶部、保持原有位置或滚动到指定元素
- 支持平滑滚动和自定义滚动逻辑
配置滚动行为
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
// 路由配置...
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
// 配置滚动行为
scrollBehavior(to, from, savedPosition) {
// to: 即将进入的目标路由对象
// from: 当前离开的路由对象
// savedPosition: 浏览器历史记录中的滚动位置
// 示例1:如果有保存的位置(如浏览器前进后退),恢复到该位置
if (savedPosition) {
return savedPosition
}
// 示例2:否则滚动到顶部
else {
return { top: 0 }
}
}
})
export default router2. 滚动行为的高级配置
1. 滚动到指定位置
scrollBehavior(to, from, savedPosition) {
// 示例:如果有保存的位置,恢复到该位置
if (savedPosition) {
return savedPosition
}
// 示例:滚动到指定位置
if (to.hash) {
return {
el: to.hash, // 滚动到指定元素
behavior: 'smooth' // 平滑滚动
}
}
// 示例:根据查询参数滚动到指定位置
if (to.query.scrollTo) {
return {
top: parseInt(to.query.scrollTo as string),
behavior: 'smooth'
}
}
// 默认滚动到顶部
return { top: 0 }
}2. 滚动到指定元素
scrollBehavior(to, from, savedPosition) {
// 示例:滚动到ID为"section-1"的元素
if (to.name === 'about' && !savedPosition) {
return {
el: '#section-1',
behavior: 'smooth'
}
}
return { top: 0 }
}3. 条件滚动行为
scrollBehavior(to, from, savedPosition) {
// 示例:根据路由元信息决定是否滚动到顶部
if (to.meta.keepScrollPosition && savedPosition) {
return savedPosition
}
// 示例:根据路由名称决定滚动行为
if (to.name === 'long-page') {
return {
top: 0,
behavior: 'smooth'
}
}
return { top: 0 }
}3. 过渡动画基础
什么是过渡动画
- 过渡动画是指在元素进入、离开或更新时应用的动画效果
- Vue提供了内置的
<transition>和<transition-group>组件 - 支持CSS过渡和CSS动画
- 支持JavaScript钩子函数
- 可以结合Vue Router实现页面切换动画
基本过渡动画
<!-- App.vue -->
<template>
<div>
<!-- 基本过渡动画 -->
<transition name="fade">
<router-view />
</transition>
</div>
</template>
<style>
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>4. 路由过渡动画
1. 基于路由元信息的过渡动画
// src/router/index.ts
const routes: Array<RouteRecordRaw> = [
{
path: '/home',
name: 'home',
component: () => import('../views/HomeView.vue'),
meta: {
transition: 'fade' // 淡入淡出动画
}
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: {
transition: 'slide' // 滑动动画
}
},
{
path: '/contact',
name: 'contact',
component: () => import('../views/ContactView.vue'),
meta: {
transition: 'zoom' // 缩放动画
}
}
]<!-- App.vue -->
<template>
<div>
<!-- 根据路由元信息动态应用过渡动画 -->
<transition :name="$route.meta.transition || 'default'">
<router-view />
</transition>
</div>
</template>
<style>
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 滑动动画 */
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(100%);
}
.slide-leave-to {
transform: translateX(-100%);
}
/* 缩放动画 */
.zoom-enter-active,
.zoom-leave-active {
transition: all 0.3s ease;
}
.zoom-enter-from,
.zoom-leave-to {
opacity: 0;
transform: scale(0.8);
}
/* 默认动画 */
.default-enter-active,
.default-leave-active {
transition: all 0.3s ease;
}
.default-enter-from,
.default-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>2. 基于路由名称的过渡动画
<!-- App.vue -->
<template>
<div>
<!-- 根据路由名称动态应用过渡动画 -->
<transition :name="getTransitionName()">
<router-view />
</transition>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 根据路由名称获取过渡动画名称
const getTransitionName = computed(() => {
const transitionMap: Record<string, string> = {
home: 'fade',
about: 'slide',
contact: 'zoom'
}
return transitionMap[route.name as string] || 'default'
})
</script>3. 基于路由方向的过渡动画
<!-- App.vue -->
<template>
<div>
<!-- 根据路由方向动态应用过渡动画 -->
<transition :name="transitionName">
<router-view />
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const transitionName = ref('fade')
// 路由白名单,用于判断导航方向
const routeOrder = ['home', 'about', 'contact', 'profile']
watch(
() => route.name,
(newName, oldName) => {
if (!newName || !oldName) {
transitionName.value = 'fade'
return
}
const newIndex = routeOrder.indexOf(newName as string)
const oldIndex = routeOrder.indexOf(oldName as string)
// 根据索引判断导航方向
if (newIndex > oldIndex) {
transitionName.value = 'slide-left'
} else if (newIndex < oldIndex) {
transitionName.value = 'slide-right'
} else {
transitionName.value = 'fade'
}
}
)
</script>
<style>
/* 从左向右滑动 */
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform 0.3s ease;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
/* 从右向左滑动 */
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 0.3s ease;
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}
</style>5. 滚动行为与过渡动画的结合
1. 先滚动后动画
// src/router/index.ts
scrollBehavior(to, from, savedPosition) {
// 先滚动到指定位置,然后应用过渡动画
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth',
// 延迟滚动,等待过渡动画完成
delay: 300
}
}
return { top: 0, behavior: 'smooth' }
}2. 结合滚动动画的过渡效果
<!-- App.vue -->
<template>
<div>
<!-- 结合滚动动画的过渡效果 -->
<transition name="fade">
<router-view v-slot="{ Component }">
<div class="page-container" ref="pageContainer">
<component :is="Component" />
</div>
</router-view>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const pageContainer = ref<HTMLElement | null>(null)
// 监听路由变化,滚动到顶部
watch(
() => route.path,
() => {
if (pageContainer.value) {
pageContainer.value.scrollTop = 0
}
}
)
onMounted(() => {
// 初始化时滚动到顶部
if (pageContainer.value) {
pageContainer.value.scrollTop = 0
}
})
</script>
<style>
.page-container {
height: 100vh;
overflow-y: auto;
scroll-behavior: smooth;
}
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>6. 高级过渡动画技巧
1. 多元素过渡
<!-- App.vue -->
<template>
<div>
<!-- 多元素过渡 -->
<transition name="fade" mode="out-in">
<router-view :key="$route.fullPath" />
</transition>
</div>
</template>
<style>
/* 淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>2. 过渡动画的JavaScript钩子
<!-- App.vue -->
<template>
<div>
<!-- 带有JavaScript钩子的过渡动画 -->
<transition
name="custom"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<router-view />
</transition>
</div>
</template>
<script setup lang="ts">
// 进入动画钩子
function beforeEnter(el: HTMLElement) {
el.style.opacity = '0'
el.style.transform = 'translateY(20px)'
}
function enter(el: HTMLElement, done: () => void) {
// 使用requestAnimationFrame确保样式已应用
requestAnimationFrame(() => {
el.style.transition = 'all 0.3s ease'
el.style.opacity = '1'
el.style.transform = 'translateY(0)'
// 监听过渡结束事件
el.addEventListener('transitionend', done)
})
}
function afterEnter(el: HTMLElement) {
// 清理事件监听器
el.removeEventListener('transitionend', () => {})
}
function enterCancelled(el: HTMLElement) {
// 处理进入动画取消
el.style.transition = ''
}
// 离开动画钩子
function beforeLeave(el: HTMLElement) {
el.style.opacity = '1'
el.style.transform = 'translateY(0)'
}
function leave(el: HTMLElement, done: () => void) {
requestAnimationFrame(() => {
el.style.transition = 'all 0.3s ease'
el.style.opacity = '0'
el.style.transform = 'translateY(-20px)'
el.addEventListener('transitionend', done)
})
}
function afterLeave(el: HTMLElement) {
el.removeEventListener('transitionend', () => {})
}
function leaveCancelled(el: HTMLElement) {
el.style.transition = ''
}
</script>3. 使用第三方动画库
<!-- App.vue -->
<template>
<div>
<!-- 使用GSAP的过渡动画 -->
<transition
@enter="enter"
@leave="leave"
>
<router-view />
</transition>
</div>
</template>
<script setup lang="ts">
import gsap from 'gsap'
// 使用GSAP实现进入动画
function enter(el: HTMLElement) {
gsap.fromTo(el,
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.3, ease: 'power2.out' }
)
}
// 使用GSAP实现离开动画
function leave(el: HTMLElement) {
gsap.fromTo(el,
{ opacity: 1, y: 0 },
{ opacity: 0, y: -20, duration: 0.3, ease: 'power2.in' }
)
}
</script>7. 最佳实践
选择合适的过渡动画
- 根据页面内容选择合适的过渡动画
- 保持动画的一致性和流畅性
- 避免过度使用复杂动画
优化性能
- 使用CSS过渡和动画,避免JavaScript动画
- 避免在动画中修改布局属性
- 使用
will-change属性优化动画性能
考虑用户体验
- 提供清晰的导航反馈
- 动画时长不宜过长(建议0.3-0.5秒)
- 支持用户关闭动画(无障碍设计)
结合滚动行为
- 合理配置滚动行为,提升导航体验
- 支持平滑滚动
- 根据路由元信息定制滚动行为
响应式设计
- 为不同设备优化过渡动画
- 考虑触摸设备的交互体验
💡 常见问题与解决方案
过渡动画不生效
- 检查是否正确使用了
<transition>组件 - 确保路由视图是唯一的根元素
- 检查CSS类名是否与transition的name属性匹配
- 检查是否正确使用了
滚动行为不生效
- 确保使用的是HTML5 History模式
- 检查scrollBehavior配置是否正确
- 确保路由切换是通过Vue Router进行的
动画性能问题
- 使用CSS变换(transform)和透明度(opacity)属性
- 避免在动画中修改top、left等布局属性
- 使用
will-change属性优化动画性能
滚动位置不一致
- 确保页面内容高度一致
- 考虑使用固定定位的元素
- 结合滚动行为和过渡动画
📚 进一步学习资源
🎯 课后练习
基础练习
- 配置基本的滚动行为
- 实现淡入淡出的过渡动画
- 基于路由元信息实现不同的过渡动画
进阶练习
- 实现基于路由方向的过渡动画
- 使用JavaScript钩子实现复杂的过渡效果
- 结合GSAP实现高级动画效果
实战练习
- 构建一个包含多种过渡动画的单页应用
- 实现响应式的滚动行为
- 优化动画性能
性能优化练习
- 分析过渡动画的性能
- 优化滚动行为的性能
- 测试不同设备上的动画效果
通过本集的学习,你已经掌握了Vue Router 4.x中滚动行为的配置、过渡动画的实现以及两者的结合使用。在实际项目中,合理运用滚动行为和过渡动画,能够提升单页应用的用户体验,使应用更加流畅、吸引人。下一集我们将深入学习路由懒加载与代码分割,进一步提升Vue 3路由系统的性能。