技术实现:滚动菜单联动与定位

核心思路

通过监听滚动事件计算当前视口位置,动态匹配对应菜单项并高亮显示。点击菜单时平滑滚动到对应区块,实现双向联动效果。以下是三种技术栈的实现方案。


原生JS+CSS+HTML方案

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>ScrollSpy Demo</title>
  <style>
    body {
      margin: 0;
      font-family: sans-serif;
      display: flex;
    }
    /* 左侧菜单 */
    .menu {
      position: fixed;
      top: 0;
      left: 0;
      width: 150px;
      height: 100vh;
      background: #f7f7f7;
      border-right: 1px solid #ddd;
      display: flex;
      flex-direction: column;
    }
    .menu-item {
      padding: 12px;
      cursor: pointer;
      transition: background 0.2s;
    }
    .menu-item:hover {
      background: #eee;
    }
    .menu-item.active {
      background: #007bff;
      color: white;
      font-weight: bold;
    }
    /* 右侧内容 */
    .content {
      margin-left: 160px;
      padding: 20px;
      width: calc(100% - 160px);
    }
    .section {
      margin-bottom: 40px;
      border: 1px solid #ccc;
      border-radius: 6px;
      padding: 20px;
      background: #fafafa;
    }
    .section:nth-child(odd) {
      background: #f0f8ff;
    }
  </style>
</head>
<body>
  <!-- 菜单 -->
  <div class="menu" id="menu"></div>

  <!-- 内容 -->
  <div class="content" id="content"></div>

  <script>
    const menuEl = document.getElementById("menu");
    const contentEl = document.getElementById("content");

    // 生成 10 个菜单和内容块
    const sectionCount = 10;
    const sections = [];
    for (let i = 0; i < sectionCount; i++) {
      // 菜单项
      const menuItem = document.createElement("div");
      menuItem.className = "menu-item";
      menuItem.innerText = `菜单 ${i + 1}`;
      menuItem.dataset.index = i;
      menuEl.appendChild(menuItem);

      // 内容区块
      const section = document.createElement("div");
      section.className = "section";
      section.id = `section-${i}`;
      section.innerHTML = `<h2>内容区 ${i + 1}</h2>
        <p>这里是第 ${i + 1} 个内容块,高度随机。</p>`;
      // 随机高度
      section.style.height = `${200 + Math.random() * 400}px`;
      contentEl.appendChild(section);
      sections.push(section);
    }

    const menuItems = document.querySelectorAll(".menu-item");

    // 点击菜单 → 滚动到对应区块
    menuItems.forEach(item => {
      item.addEventListener("click", () => {
        const index = parseInt(item.dataset.index, 10);
        sections[index].scrollIntoView({ behavior: "smooth", block: "start" });
      });
    });

    // 滚动监听 → 切换菜单高亮
    window.addEventListener("scroll", () => {
      let current = 0;
      sections.forEach((sec, i) => {
        const rect = sec.getBoundingClientRect();
        if (rect.top <= 100) { // 进入视口一定范围时认为是当前
          current = i;
        }
      });
      menuItems.forEach((item, i) => {
        item.classList.toggle("active", i === current);
      });
    });
  </script>
</body>
</html>

Vue 3 方案(Composition API)

组件结构
<template>
  <div class="menu">
    <ul>
      <li v-for="(item, index) in sections" :key="index">
        <a 
          :href="`#${item.id}`"
          :class="{ active: activeSection === item.id }"
          @click.prevent="scrollTo(item.id)"
        >{{ item.title }}</a>
      </li>
    </ul>
  </div>
  <div class="content">
    <section 
      v-for="(item, index) in sections" 
      :id="item.id" 
      :key="index" 
      class="section"
    >{{ item.content }}</section>
  </div>
</template>

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

const sections = ref([
  { id: 'sec1', title: 'Section 1', content: '...' },
  { id: 'sec2', title: 'Section 2', content: '...' },
  { id: 'sec3', title: 'Section 3', content: '...' }
]);
const activeSection = ref('');

const handleScroll = () => {
  sections.value.forEach(section => {
    const el = document.getElementById(section.id);
    const rect = el.getBoundingClientRect();
    if (rect.top <= 100 && rect.bottom >= 100) {
      activeSection.value = section.id;
    }
  });
};

const scrollTo = (id) => {
  const el = document.getElementById(id);
  el.scrollIntoView({ behavior: 'smooth' });
};

onMounted(() => {
  window.addEventListener('scroll', handleScroll);
  // 初始化检测第一个可见区块
  handleScroll();
});
</script>

动态高度处理

通过ResizeObserver监听区块高度变化:

const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    const sectionId = entry.target.id;
    // 更新对应section的高度记录
  });
});

onMounted(() => {
  sections.value.forEach(section => {
    observer.observe(document.getElementById(section.id));
  });
});


性能优化建议

节流处理滚动事件
let isScrolling;
window.addEventListener('scroll', () => {
  window.clearTimeout(isScrolling);
  isScrolling = setTimeout(() => {
    handleScroll();
  }, 100);
});

Intersection Observer API替代

现代浏览器推荐使用IntersectionObserver实现更高效的元素可见性检测:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      activeSection.value = entry.target.id;
    }
  });
}, { threshold: 0.5 });

onMounted(() => {
  sections.value.forEach(section => {
    observer.observe(document.getElementById(section.id));
  });
});


关键问题解决方案

动态生成菜单与区块
  1. 通过v-for循环渲染动态数据
  2. 使用唯一ID作为锚点标识
  3. 通过计算属性生成菜单项数据
高度不一致处理
  1. 使用ResizeObserver监听高度变化
  2. 存储各区块的offsetTop位置信息
  3. 滚动时实时计算相对位置
跨浏览器兼容性
  1. 添加scroll-margin-top的polyfill
  2. 平滑滚动使用behavior: 'smooth'的polyfill
  3. 使用requestAnimationFrame优化滚动性能
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐