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 router

2. 滚动行为的高级配置

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提供了内置的&lt;transition&gt;&lt;transition-group&gt;组件
  • 支持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. 最佳实践

  1. 选择合适的过渡动画

    • 根据页面内容选择合适的过渡动画
    • 保持动画的一致性和流畅性
    • 避免过度使用复杂动画
  2. 优化性能

    • 使用CSS过渡和动画,避免JavaScript动画
    • 避免在动画中修改布局属性
    • 使用will-change属性优化动画性能
  3. 考虑用户体验

    • 提供清晰的导航反馈
    • 动画时长不宜过长(建议0.3-0.5秒)
    • 支持用户关闭动画(无障碍设计)
  4. 结合滚动行为

    • 合理配置滚动行为,提升导航体验
    • 支持平滑滚动
    • 根据路由元信息定制滚动行为
  5. 响应式设计

    • 为不同设备优化过渡动画
    • 考虑触摸设备的交互体验

💡 常见问题与解决方案

  1. 过渡动画不生效

    • 检查是否正确使用了&lt;transition&gt;组件
    • 确保路由视图是唯一的根元素
    • 检查CSS类名是否与transition的name属性匹配
  2. 滚动行为不生效

    • 确保使用的是HTML5 History模式
    • 检查scrollBehavior配置是否正确
    • 确保路由切换是通过Vue Router进行的
  3. 动画性能问题

    • 使用CSS变换(transform)和透明度(opacity)属性
    • 避免在动画中修改top、left等布局属性
    • 使用will-change属性优化动画性能
  4. 滚动位置不一致

    • 确保页面内容高度一致
    • 考虑使用固定定位的元素
    • 结合滚动行为和过渡动画

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 配置基本的滚动行为
    • 实现淡入淡出的过渡动画
    • 基于路由元信息实现不同的过渡动画
  2. 进阶练习

    • 实现基于路由方向的过渡动画
    • 使用JavaScript钩子实现复杂的过渡效果
    • 结合GSAP实现高级动画效果
  3. 实战练习

    • 构建一个包含多种过渡动画的单页应用
    • 实现响应式的滚动行为
    • 优化动画性能
  4. 性能优化练习

    • 分析过渡动画的性能
    • 优化滚动行为的性能
    • 测试不同设备上的动画效果

通过本集的学习,你已经掌握了Vue Router 4.x中滚动行为的配置、过渡动画的实现以及两者的结合使用。在实际项目中,合理运用滚动行为和过渡动画,能够提升单页应用的用户体验,使应用更加流畅、吸引人。下一集我们将深入学习路由懒加载与代码分割,进一步提升Vue 3路由系统的性能。

« 上一篇 Vue3 + TypeScript 系列教程 - 第67集:路由守卫深度解析 下一篇 » Vue3 + TypeScript 系列教程 - 第69集:路由懒加载与代码分割