vue3实现锚点滚动与页面滚动高亮选中

247 阅读4分钟

vue3实现锚点滚动与页面滚动高亮选中

引言

在现代网页设计中,锚点滚动和滚动高亮是常见的交互功能,尤其在单页应用(SPA)中。用户在页面中通过点击导航菜单,可以直接跳转到对应的内容区域。而当用户滚动页面时,能够实时标记出当前可视区域的内容,提升用户的定位体验。本文将介绍如何利用 scrollTogetBoundingClientRect 实现精准的锚点滚动与滚动高亮功能,并深入探讨如何解决滚动过程中可能遇到的一些问题。

我们将逐步实现:

  1. 使用 scrollTo 精准控制页面滚动。
  2. 根据滚动位置,判断元素是否进入视口并高亮显示。
  3. 解决高亮跳动和性能问题。

实现步骤

1. 锚点滚动功能

实现锚点滚动时,最常见的方式是使用 scrollIntoView,它能够让元素滚动到视口中。然而,在一些复杂布局中,它的滚动位置可能存在误差。相比之下,scrollTo 可以通过手动计算元素的 offsetTop,更精确地控制滚动位置。

const scrollToTarget = (targetId) => {
    const targetElement = document.getElementById(targetId);
    if (targetElement) {
        const offsetTop = targetElement.offsetTop; // 获取目标元素的垂直偏移位置
        window.scrollTo({
            top: offsetTop,
            behavior: 'smooth', // 平滑滚动
        });
    }
};

通过这种方式,我们可以精确地滚动到目标元素,无论页面中的其他元素如何排列。


2. 页面滚动高亮选中

为了在用户滚动时标记出当前可视区域的内容,需要通过 getBoundingClientRect 获取目标元素相对于视口的位置,然后判断元素是否进入了视口的一部分,若进入则触发高亮。

const pageScroll = () => {
    if (isScrolling) {
        console.log("Ignoring scroll event during scrolling");
        return;
    }

    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    let currentActive = activeSection.value;

    for (let i = 0; i < sections.length; i++) {
        const section = document.getElementById(sections[i]);
        if (!section) continue;

        const rect = section.getBoundingClientRect();

        // 向下滚动:元素顶部进入视口
        if (rect.top <= viewportHeight / 2 && rect.top >= 0) {
            currentActive = sections[i];
            break;
        }

        // 向上滚动:元素底部进入视口
        if (rect.bottom >= viewportHeight / 2 && rect.bottom <= viewportHeight) {
            currentActive = sections[i];
            break;
        }
    }

    if (currentActive !== activeSection.value) {
        activeSection.value = currentActive; // 更新高亮区域
    }
};

这样,在页面滚动时,会实时更新当前视口内的高亮内容。


3. 解决滚动过程中高亮跳动问题

在使用 scrollTo 滚动页面时,scroll 事件会被触发,从而导致高亮状态的跳动。为了解决这个问题,我们可以通过引入一个额外的状态值来控制是否触发滚动事件,并使用滚动结束检测机制来确保状态同步。

检测滚动是否结束:

let isScrolling = false;
let lastScrollPosition = 0;
let scrollTimeout = null;

const detectScrollEnd = (callback) => {
    if (scrollTimeout) clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(() => {
        const currentScrollPosition = window.scrollY;
        if (Math.abs(currentScrollPosition - lastScrollPosition) < 1) {
            callback(); // 滚动结束,执行回调
        } else {
            lastScrollPosition = currentScrollPosition;
            detectScrollEnd(callback); // 继续检测滚动状态
        }
    }, 100); // 每隔100毫秒检测一次滚动位置
};

const handleScroll = () => {
    detectScrollEnd(() => {
        isScrolling = false;
    });
};

window.addEventListener('scroll', handleScroll);

通过这种方式,我们能够有效地避免滚动过程中的高亮跳动问题。


踩坑点与解决方案

  1. scrollIntoView 滚动偏差
    解决方法:采用 scrollTo 方法,手动计算滚动位置,确保精准控制滚动距离。

  2. 高亮状态跳动
    解决方法:在滚动过程中通过 isScrolling 标志位来避免滚动事件重复触发,同时加入滚动结束检测,确保状态同步。

  3. 性能优化

    • 在滚动过程中,避免频繁访问 DOM,尽量减少不必要的计算和布局重新渲染。
    • 可以结合 requestAnimationFramesetTimeout 来进行滚动优化,提升滚动性能。

总结

通过本文,我们详细探讨了如何实现锚点滚动与页面滚动高亮选中,并解决了常见的滚动偏差、高亮跳动等问题。我们利用 scrollTo 实现精准滚动,使用 getBoundingClientRect 判断元素位置并进行高亮,同时通过检测滚动结束来避免状态同步问题。最终的方案不仅能提高用户体验,还能保证性能和稳定性。


完整代码


// 滚动到目标元素
const scrollToTarget = (targetId) => {
    const targetElement = document.getElementById(targetId);
    if (targetElement) {
        const offsetTop = targetElement.offsetTop;
        window.scrollTo({
            top: offsetTop,
            behavior: 'smooth',
        });
    }
};

// 滚动高亮逻辑
const pageScroll = () => {
    if (isScrolling) {
        console.log("Ignoring scroll event during scrolling");
        return;
    }

    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    let currentActive = activeSection.value;

    for (let i = 0; i < sections.length; i++) {
        const section = document.getElementById(sections[i]);
        if (!section) continue;

        const rect = section.getBoundingClientRect();

        if (rect.top <= viewportHeight / 2 && rect.top >= 0) {
            currentActive = sections[i];
            break;
        }

        if (rect.bottom >= viewportHeight / 2 && rect.bottom <= viewportHeight) {
            currentActive = sections[i];
            break;
        }
    }

    if (currentActive !== activeSection.value) {
        activeSection.value = currentActive;
    }
};

// 检测滚动结束
const detectScrollEnd = (callback) => {
    if (scrollTimeout) clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(() => {
        const currentScrollPosition = window.scrollY;
        if (Math.abs(currentScrollPosition - lastScrollPosition) < 1) {
            callback();
        } else {
            lastScrollPosition = currentScrollPosition;
            detectScrollEnd(callback);
        }
    }, 100);
};

// 初始化和卸载事件
useEffect(() => {
    window.addEventListener('scroll', pageScroll);

    return () => {
        window.removeEventListener('scroll', pageScroll);
    };
}, []);