页面的滚动动画与平滑滚动

369 阅读13分钟

最近有个项目是关于公司产品宣传页的开发,产品设计的页面时有关于页面滚动的一些小动画,乘此机会就总结了一些常用的实现方法。

在现代 Web 应用中,流畅的用户体验至关重要。滚动,作为用户与页面交互最频繁的操作之一,其表现直接影响着用户对网站或应用的第一印象。生硬的跳转、卡顿的动画都会让体验大打折扣。而优雅的平滑滚动和适时的滚动触发动画,则能极大地提升页面的专业感和用户的愉悦度。

今天,我们将聚焦于如何在 Vue 3 项目中实现这些效果,并探讨几种不同的方法,分析它们的优劣,帮助你在不同场景下做出最佳选择。


📜 本文目标

  1. 理解平滑滚动的基本概念和实现方式。
  2. 掌握在 Vue 3 中实现页面内平滑滚动到指定元素的多种技术。
  3. 学习如何实现元素根据页面滚动位置触发动画(进入视口动画)。
  4. 对比不同方法的优缺点及适用场景。

🚀 方法一:CSS scroll-behavior: smooth;

这是最简单、最原生、也是首先应该考虑的方法。CSS scroll-behavior 属性允许你为滚动容器(通常是 <html> 或某个可滚动 div)定义滚动行为。

原理: 浏览器原生支持,当用户点击指向页面内部锚点(#hash)的链接,或者通过 JavaScript 的 element.scrollTo()window.scrollTo()注意:不带 behavior 选项或 behavior: 'auto' 时不会平滑)进行导航时,如果滚动容器设置了 scroll-behavior: smooth;,浏览器会自动处理平滑滚动动画。

适用场景: 主要用于页面内锚点链接的平滑过渡。

Vue 3 示例代码:

<template>
  <div class="scroll-container">
    <nav>
      <a href="#section1">Go to Section 1</a>
      <a href="#section2">Go to Section 2</a>
      <a href="#section3">Go to Section 3</a>
    </nav>

    <section id="section1">Section 1</section>
    <section id="section2">Section 2</section>
    <section id="section3">Section 3</section>
  </div>
</template>

<style>
/* 应用于根元素,影响整个页面的滚动 */
html {
  scroll-behavior: smooth;
}

/* 或者,如果滚动发生在特定容器内 */
.scroll-container {
  /* height: 100vh; */
  /* overflow-y: auto; */
  /* scroll-behavior: smooth;  <-- 应用于容器 */
}

/* 仅为示例添加样式 */
nav {
  position: sticky;
  top: 0;
  background: #eee;
  padding: 10px;
  z-index: 10;
  text-align: center;
}
nav a {
  margin: 0 15px;
  text-decoration: none;
  color: blue;
}
section {
  height: 100vh; /* 使页面足够长可以滚动 */
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 3em;
  border-bottom: 2px solid #ccc;
}
#section1 { background-color: lightblue; }
#section2 { background-color: lightcoral; }
#section3 { background-color: lightgreen; }
</style>

1.gif

优点:

  • 极简: 一行 CSS 代码即可实现。
  • 原生高效: 浏览器底层实现,性能通常很好。
  • 无需 JS: 对于纯锚点链接跳转,完全无需 JavaScript。

缺点:

  • 兼容性: 虽然现代浏览器支持良好,但仍需注意老旧浏览器(IE 不支持)。
  • 控制力弱: 无法自定义动画曲线(easing function)、滚动速度和持续时间。
  • 场景局限: 主要适用于锚点链接跳转,对于通过 JS 触发的、需要精细控制的滚动场景不够灵活。

🚀 方法二:JavaScript window.scrollTo()/element.scrollTo()

当我们需要通过 JavaScript 动态触发滚动(例如,点击按钮滚动到页面某个区域),或者需要兼容不支持 CSS scroll-behavior 的浏览器时,可以使用 Web API scrollTo()scrollBy()

原理: window.scrollTo(options)element.scrollTo(options) 方法接受一个配置对象,其中 behavior: 'smooth' 选项可以指示浏览器执行平滑滚动。

适用场景: 任何需要通过 JavaScript 控制的滚动,如按钮点击、程序逻辑触发等。

Vue 3 示例代码:

<template>
  <div>
    <nav>
      <button @click="scrollToSection('section1')">Go to Section 1</button>
      <button @click="scrollToSection('section2')">Go to Section 2</button>
      <button @click="scrollToElement(section3Ref)">Go to Section 3 (Ref)</button>
    </nav>

    <section id="section1">Section 1</section>
    <section id="section2">Section 2</section>
    <section ref="section3Ref" style="background-color: lightgreen;">Section 3</section>

    <!-- 更多内容撑开页面 -->
    <div style="height: 100vh; background: #eee;">More Content</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const section3Ref = ref(null);

// 方法一:通过 ID 获取元素
const scrollToSection = (sectionId) => {
  const element = document.getElementById(sectionId);
  if (element) {
    // element.offsetTop 获取元素相对于 offsetParent 的顶部距离
    // 如果是滚动整个页面,通常 offsetTop 就足够
    // 注意:如果存在 fixed 的 header,需要计算偏移量
    const headerOffset = 0; // 如有 fixed header,设置其高度
    const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
    const offsetPosition = elementPosition - headerOffset;

    window.scrollTo({
      top: offsetPosition,
      behavior: 'smooth'
    });

    // 或者,如果滚动发生在特定容器内:
    // containerElement.scrollTo({ top: element.offsetTop - headerOffset, behavior: 'smooth' });
  }
};

// 方法二:通过 Vue ref 获取元素
const scrollToElement = (element) => {
  if (element) {
     const headerOffset = 0;
     // getBoundingClientRect().top 相对于视口顶部
     // window.pageYOffset (或 window.scrollY) 当前滚动距离
     const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
     const offsetPosition = elementPosition - headerOffset;

     window.scrollTo({
        top: offsetPosition,
        behavior: 'smooth'
     });
  }
};

// 注意:使用 getBoundingClientRect 通常更可靠,因为它考虑了所有父元素的定位。
// 而 offsetTop 只相对于最近的 positioned (relative, absolute, fixed, sticky) 父元素或 body。
</script>

<style>
/* 沿用上例部分样式 */
nav {
  position: sticky;
  top: 0;
  background: #eee;
  padding: 10px;
  z-index: 10;
  text-align: center;
}
button {
  margin: 0 10px;
  padding: 5px 10px;
  cursor: pointer;
}
section {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 3em;
  border-bottom: 2px solid #ccc;
}
#section1 { background-color: lightblue; }
#section2 { background-color: lightcoral; }
/* section3 通过 ref 获取,样式内联 */
</style>

2.gif

优点:

  • 更灵活: 可以由任何 JS 事件触发。
  • 原生支持: 大多数现代浏览器支持 behavior: 'smooth'
  • 结合 Vue Ref: 能方便地滚动到由 ref 引用的 Vue 组件或 DOM 元素。

缺点:

  • 控制力仍有限: 仍然无法自定义动画曲线、速度和持续时间。
  • 兼容性: behavior: 'smooth' 在老浏览器(如 IE)中不支持,需要 Polyfill 或降级处理(瞬间滚动)。
  • 偏移计算: 可能需要手动计算固定头部(Fixed Header)的偏移量。

🚀 方法三:使用第三方动画库 (如 GSAP + ScrollTrigger)

对于需要精细控制滚动动画,或者实现复杂的滚动触发效果(如视差滚动、元素随滚动缩放/旋转/固定等),强大的 JavaScript 动画库是更好的选择。GSAP (GreenSock Animation Platform) 配合其 ScrollTrigger 插件是业界标杆。

原理: GSAP 提供了强大的补间动画引擎,ScrollTrigger 则将这些动画与页面的滚动位置关联起来。它可以触发动画、控制动画进度、固定元素等。对于平滑滚动,GSAP 也可以通过 scrollTo 插件实现,提供更多自定义选项。

适用场景:

  • 需要自定义滚动动画曲线、时长。
  • 实现复杂的滚动触发动画(元素入场/出场、视差效果等)。
  • 需要统一管理页面中的多种动画。

Vue 3 示例代码:

# 安装 GSAP
npm install gsap
# 或者
yarn add gsap
<template>
  <div>
    <nav>
      <button @click="smoothScrollTo('#section1')">GSAP Scroll To Section 1</button>
      <button @click="smoothScrollTo('#section2')">GSAP Scroll To Section 2</button>
    </nav>

    <section id="section1">Section 1</section>
    <section id="section2">
        Section 2
        <div class="box animate-on-scroll">Animate Me!</div>
    </section>
    <section id="section3">Section 3</section>

    <div style="height: 100vh; background: #eee;">More Content</div>
  </div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';
import { gsap } from 'gsap';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

// 注册插件
gsap.registerPlugin(ScrollToPlugin, ScrollTrigger);

// 平滑滚动函数
const smoothScrollTo = (target) => {
  gsap.to(window, {
    duration: 1, // 滚动持续时间 (秒)
    scrollTo: {
      y: target, // 目标选择器或 y 坐标
      offsetY: 70 // 考虑固定导航栏的高度偏移
    },
    ease: 'power3.inOut' // 缓动函数 (easing)
  });
};

// 滚动触发动画
onMounted(() => {
  // 查找所有需要动画的元素
  gsap.utils.toArray('.animate-on-scroll').forEach(box => {
    gsap.from(box, { // 定义进入动画
      opacity: 0,
      y: 100,
      duration: 0.8,
      scrollTrigger: {
        trigger: box, // 触发动画的元素
        start: 'top 80%', // 当元素顶部进入视口 80% 时触发
        // end: 'bottom 20%', // 可选:动画结束条件
        toggleActions: 'play none none reverse', // 进入时播放,离开时反向播放
        // markers: true, // 调试用,显示触发器位置
      }
    });
  });
});

// 清理 ScrollTrigger 实例,防止内存泄漏 (虽然 GSAP 3 通常会自动处理)
onUnmounted(() => {
  ScrollTrigger.getAll().forEach(trigger => trigger.kill());
});
</script>

<style>
/* 沿用上例部分样式 */
nav {
  position: sticky; /* 注意:需要 position sticky/fixed 才能应用 offsetY */
  top: 0;
  height: 70px; /* 假设导航栏高度 70px */
  background: #eee;
  padding: 10px;
  z-index: 10;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
button { margin: 0 10px; padding: 5px 10px; cursor: pointer; }
section {
  height: 100vh;
  display: flex;
  flex-direction: column; /* 为了演示内部元素动画 */
  justify-content: center;
  align-items: center;
  font-size: 3em;
  border-bottom: 2px solid #ccc;
  position: relative; /* ScrollTrigger 可能需要 */
}
#section1 { background-color: lightblue; }
#section2 { background-color: lightcoral; }
#section3 { background-color: lightgoldenrodyellow; }

.box {
  width: 150px;
  height: 150px;
  background-color: navy;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 30px;
  font-size: 0.5em;
  /* 初始状态可以设置在这里,但 GSAP 的 from/fromTo 更常用 */
}
</style>

3.gif

优点:

  • 极致控制: 完全控制动画时长、缓动曲线、延迟等。
  • 功能强大: ScrollTrigger 支持各种复杂的滚动交互效果。
  • 生态成熟: GSAP 社区活跃,文档完善,性能优异。
  • 跨浏览器兼容: GSAP 内部处理了兼容性问题。

缺点:

  • 引入依赖: 需要额外引入库文件,增加项目体积。
  • 学习曲线: 相较于原生方法,需要学习 GSAP 和 ScrollTrigger 的 API。

🚀 方法四:使用 Vue 生态库 (如 vueuse)

VueUse 是一个流行的 Vue Composition Utilities 集合库,它提供了许多有用的组合式函数,包括与滚动相关的工具。

原理: VueUse 提供了 useScroll 组合式函数,可以方便地、响应式地跟踪窗口或某个元素的滚动位置。你可以基于这些响应式数据,结合 Vue 的 watchcomputed 来触发 CSS 过渡/动画,或者手动调用 scrollTo。它也提供了 useScrollTo (非官方,但社区有实现)或可以自己封装一个基于 useScroll 的平滑滚动函数。对于滚动动画,可以结合 useIntersectionObserver 来检测元素是否进入视口。

适用场景:

  • 需要响应式地获取滚动状态(位置、方向等)。
  • 实现基于滚动位置的简单逻辑或 UI 变化。
  • 检测元素是否进入/离开视口以触发动画或懒加载。
  • 希望保持在 Vue 生态内解决问题。

Vue 3 示例代码:

# 安装 VueUse
npm install @vueuse/core
# 或者
yarn add @vueuse/core
<template>
  <div>
    <nav :class="{ 'nav-scrolled': y > 50 }">
      Scroll Position Y: {{ y.toFixed(0) }}
      <button @click="smoothScrollToTarget">VueUse Scroll To Target</button>
    </nav>

    <section class="target-section">Target Section</section>

    <section>
      <div ref="animatedElement" class="box-vueuse" :class="{ 'is-visible': elementIsVisible }">
        Animate Me (VueUse)!
      </div>
    </section>

    <div style="height: 150vh; background: #eee;">More Content</div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue';
import { useScroll, useIntersectionObserver } from '@vueuse/core';

// 1. 跟踪窗口滚动位置
const { y } = useScroll(window);

// 2. 平滑滚动 (简单实现,可封装成更强大的组合式函数)
const smoothScrollToTarget = () => {
  const targetElement = document.querySelector('.target-section');
  if (targetElement) {
    const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;
    window.scrollTo({
      top: targetPosition - 70, // 假设导航栏 70px 高
      behavior: 'smooth'
    });
  }
};

// 3. 滚动触发动画 (使用 Intersection Observer)
const animatedElement = ref(null);
const elementIsVisible = ref(false);

// useIntersectionObserver 选项
const options = {
  threshold: 0.5 // 元素可见 50% 时触发
};

const { stop } = useIntersectionObserver(
  animatedElement, // 目标元素 ref
  ([{ isIntersecting }], observerElement) => {
    // isIntersecting 是一个布尔值,表示元素是否与根相交
    elementIsVisible.value = isIntersecting;

    // 如果只需要触发一次,可以在这里停止观察
    // if (isIntersecting) {
    //   stop();
    // }
  },
  options
);

// 组件卸载时自动停止观察 (useIntersectionObserver 内部处理)
// onUnmounted(stop); // 通常不需要手动调用
</script>

<style>
nav {
  position: sticky;
  top: 0;
  height: 70px;
  background: lightgray;
  padding: 10px;
  z-index: 10;
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: space-around;
  transition: background-color 0.3s ease;
}
nav.nav-scrolled {
  background-color: lightblue;
}
button { padding: 5px 10px; cursor: pointer; }

section {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 2em;
  border-bottom: 1px solid #ddd;
}
.target-section { background-color: lightpink; }

.box-vueuse {
  width: 200px;
  height: 200px;
  background-color: darkslateblue;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 0.8em;
  transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
  opacity: 0;
  transform: translateY(50px);
}
.box-vueuse.is-visible {
  opacity: 1;
  transform: translateY(0);
}
</style>

4.gif

优点:

  • Vue 生态友好: 与 Vue 的响应式系统无缝集成。
  • 组合式函数: 代码组织清晰,逻辑复用方便。
  • 按需引入: 只引入需要的模块,相对轻量。
  • useIntersectionObserver 高效: 使用浏览器原生 API 检测元素可见性,性能优于监听 scroll 事件。

缺点:

  • 平滑滚动需自行实现或组合: useScroll 主要用于跟踪,平滑滚动逻辑还需结合 scrollTo API。
  • 复杂动画能力不如 GSAP: 对于非常复杂的序列动画或物理效果,GSAP 更专业。
  • 引入依赖: 仍然需要添加 @vueuse/core 依赖。

🛠️ 方法对比与建议

好的,这是对上述四种实现页面滚动动画和平滑滚动方法的列表式总结:

1. CSS scroll-behavior: smooth;

  • 实现方式: CSS
  • 易用性: ⭐⭐⭐⭐⭐ (非常简单)
  • 灵活性/控制力: ⭐ (低,无法自定义动画)
  • 功能丰富度: ⭐ (仅限锚点/原生 API 触发)
  • 性能: ⭐⭐⭐⭐⭐ (原生实现,高效)
  • 兼容性: ⭐⭐⭐⭐ (现代浏览器良好,需注意旧版)
  • 依赖:
  • 滚动触发动画: 不支持
  • 平滑滚动自定义: 不支持

2. JavaScript scrollTo({ behavior: 'smooth' });

  • 实现方式: Native JS API
  • 易用性: ⭐⭐⭐⭐ (较简单)
  • 灵活性/控制力: ⭐⭐ (有限,无法自定义动画曲线/时长)
  • 功能丰富度: ⭐ (仅用于触发滚动)
  • 性能: ⭐⭐⭐⭐ (原生实现,较好)
  • 兼容性: ⭐⭐⭐⭐ (需注意 behavior 选项在旧版浏览器的支持)
  • 依赖:
  • 滚动触发动画: 需手动监听 scroll 事件 (不推荐) 或结合其他 API
  • 平滑滚动自定义: 不支持

3. GSAP + ScrollTrigger

  • 实现方式: JS Library
  • 易用性: ⭐⭐ (有学习曲线)
  • 灵活性/控制力: ⭐⭐⭐⭐⭐ (极高,完全控制动画)
  • 功能丰富度: ⭐⭐⭐⭐⭐ (非常丰富,支持复杂效果)
  • 性能: ⭐⭐⭐⭐ (优化良好,但有库开销)
  • 兼容性: ⭐⭐⭐⭐⭐ (库本身处理跨浏览器问题)
  • 依赖: 重 (需要引入 GSAP 核心及插件)
  • 滚动触发动画: 内置强大支持 (ScrollTrigger)
  • 平滑滚动自定义: 完全支持 (scrollTo 插件)

4. VueUse (useScroll, useIntersectionObserver)

  • 实现方式: JS Library (Vue Composables)
  • 易用性: ⭐⭐⭐⭐ (Vue 生态内友好)
  • 灵活性/控制力:
    • 滚动状态跟踪: ⭐⭐⭐ (响应式数据)
    • 平滑滚动: ⭐⭐ (需结合原生 API)
    • 元素可见性检测: ⭐⭐⭐⭐ (使用 IntersectionObserver)
  • 功能丰富度: ⭐⭐⭐ (提供实用工具集,但非专业动画库)
  • 性能:
    • useIntersectionObserver: ⭐⭐⭐⭐ (高效)
    • useScroll (若频繁操作 DOM): ⭐⭐⭐ (优于直接监听,但仍需注意)
  • 兼容性: ⭐⭐⭐⭐⭐ (依赖的 Web API 兼容性良好)
  • 依赖: 中等 (需要引入 @vueuse/core)
  • 滚动触发动画: 通过 useIntersectionObserver 高效支持
  • 平滑滚动自定义: 需自行实现/封装 (通常结合原生 scrollTo) |

总结建议:

  1. 基础锚点链接跳转: 首选 CSS scroll-behavior: smooth;,简单高效。
  2. JS 触发简单平滑滚动: 如果不需要自定义动画且目标浏览器支持,使用 window.scrollTo({ behavior: 'smooth' })element.scrollTo()。在 Vue 中结合 ref 获取元素。
  3. 需要响应式滚动状态或高效检测元素可见性: 考虑 VueUseuseScroll 用于数据跟踪,useIntersectionObserver 用于高效触发入场动画(通常比监听 scroll 事件性能更好)。平滑滚动仍需结合原生 API。
  4. 复杂滚动触发动画、视差效果、自定义平滑滚动曲线/时长: GSAP + ScrollTrigger 是不二之选。虽然有学习曲线和依赖体积,但功能和控制力无与伦比。
  5. 性能敏感且需自定义滚动逻辑: 对于极端情况,可以考虑使用 requestAnimationFrame 手动编写滚动动画循环(这里未详述,相对复杂),但这通常只在库无法满足需求或需要极致优化时才考虑。

额外提示:

  • 固定头部偏移: 使用 JS 滚动时,务必计算并减去固定导航栏或头部的高度,确保目标元素准确滚动到视口下方。getBoundingClientRect() 通常比 offsetTop 更可靠。
  • 可访问性: 尊重用户的系统设置。可以通过 matchMedia('(prefers-reduced-motion: reduce)') 检测用户是否希望减少动画,并据此禁用平滑滚动或滚动动画。
// 示例:检测 prefers-reduced-motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

function scrollToTarget(target) {
  const element = document.querySelector(target);
  if (element) {
     const targetPosition = element.getBoundingClientRect().top + window.pageYOffset;
     window.scrollTo({
       top: targetPosition,
       behavior: prefersReducedMotion ? 'auto' : 'smooth' // 减少动效时直接跳转
     });
  }
}
  • 节流与防抖: 如果你选择手动监听 scroll 事件来实现滚动动画(不推荐,优先使用 IntersectionObserverScrollTrigger),请务必对事件处理函数进行节流(throttling)或防抖(debouncing),避免性能问题。

结语

掌握页面的滚动动画与平滑滚动是提升前端项目用户体验的关键技能。Vue 3 提供了强大的基础,结合 CSS、原生 JS API 以及优秀的第三方库(如 GSAP、VueUse),我们可以灵活地应对各种滚动相关的需求。

希望本文介绍的几种方法和对比能帮助大家在实际项目中做出明智的技术选型。选择最适合你项目需求、团队熟悉度和性能要求的方法,打造出如丝般顺滑的页面滚动体验吧!

VX公众号【前端大大大】

qrcode_for_gh_c467a0436534_344.jpg