实现一个匀速滚动的页内侧边栏导航

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

侧边栏导航也称电梯导航,通常悬浮在页面侧边作为页面目录进行使用。

主要功能

主要实现的功能有两点:

  • 点击侧边栏导航滚动到页面指定位置
  • 页面滚动到指定位置时,侧边栏自动定位

提示

案例基于vue3项目实现,但其核心原理和所使用框架并无太大关联,套用到任意一个框架中都适用。

开始实现:

  1. 页面布局

    搭建一个简单的页面案例,左侧是滚动浏览内容,右侧是侧边导航栏。

    滚动内容需要和侧边栏菜单进行联动,给主题内容块绑定一个id,以便可以获取对应的元素,这里不用vue的ref获取元素主要是因为:在正式开发中,通过会拆分组件进行开发,侧边栏通常不会和主体内容放到同一个文件里,所以ref在功能实现上不太合适。

    <template>
      <div class="main">
        <!-- 滚动内容 -->
        <section class="content" v-for="item in menu" :key="item.name" :id="item.id">
          <header class="header">
            {{item.name}}
          </header>
        </section>
    
        <!-- 侧边栏 -->
        <ul class="sidebar">
          <li v-for="item in menu" :key="item.name">
            <a href="javascript:;">{{item.name}}</a>
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import { defineComponent, ref } from 'vue';
    
    export default defineComponent({
      setup() {
        const menu = ref([
          {
            name: '前言',
            id: 'main1',
            intersectionRatio: 0, // 用来判断侧边栏的激活状态,后续有用。
          },
          {
            name: '简介',
            id: 'main2',
            intersectionRatio: 0,
          },
          {
            name: '主要功能',
            id: 'main3',
            intersectionRatio: 0,
          },
          {
            name: '阅读提示',
            id: 'main4',
            intersectionRatio: 0,
          },
          {
            name: '开始编写',
            id: 'main5',
            intersectionRatio: 0,
          },
          {
            name: '结语',
            id: 'main6',
            intersectionRatio: 0,
          },
        ])
    
        return {
          menu
        };
      },
    });
    </script>
    
    .main {
       padding: 30px;
       padding-right: 240px;
    
       .content {
         height: 70vh;
         background: #eee;
         border-radius: 8px;
         margin-bottom: 15px;
       }
    
       .header {
         padding: 15px;
         font-size: 20px;
         font-weigth: bold;
       }
     }
    
     .sidebar {
       position: fixed;
       top: 50%;
       right: 50px;
       width: 168px;
       overflow: hidden;
       background-color: #fff;
       border-radius: 8px;
       padding: 0;
       transform: translateY(-50%);
       box-shadow: 0px 1px 10px 0px rgba(0,0,0,0.05), 0px 3px 5px 0px rgba(0,0,0,0.06), 0px 2px 4px -1px rgba(0,0,0,0.04); 
    
       li {
         line-height: 40px;
         text-align: center;
         list-style: none;
    
         &:not(:last-child) {
           border-bottom: #f5f5f5 solid 1px;
         }
    
         &.active {
           color: #fff;
           background-color: #ff8000;
         }
    
         a {
           display: block;
           width: 100%;
           height: 100%;
           color: #333;
           text-decoration: none;
         }
       }
     }
    

    得到页面结构如下:

    image

  2. 点击侧边栏滚动到页面指定目录

    这一步骤最简单的方法是通过 a标签 的锚点特性实现,改变路由的hash直接定位到页面具体的id元素位置。

    但这种方式的缺点是太简单,没法实现页面滚动的动画效果,且想要实现定位时和顶部保持的一定距离很难,所以还是推荐使用window的滚动方法实现。

    给侧边栏绑定点击事件,每当侧边栏点击时,获取对应页面元素的 offsetTop,并通过 window.scrollTo 滚动对应位置即可。

    <!-- 侧边栏 -->
    <ul class="sidebar">
      <li v-for="item in menu" :key="item.name">
        <a href="javascript:;" @click="clickSideBar(item)">{{item.name}}</a>
      </li>
    </ul>
    
    function clickSideBar(item) {
      const dom = document.getElementById(item.id)
      if (dom) {
        window.scrollTo(0, dom.offsetTop - 50) // -50是为了让顶部留出距离,在页面存在顶部固定导航栏时可以使用
      }
    }
    

    这样很简单,但是缺少动画,没有灵性,我们可以改造一下:

    只要滚动的间隔和每次滚动的距离是固定的,动画就是匀速的,间隔越短,动画越流畅。

    function clickSideBar(item) {
      const dom = document.getElementById(item.id)
      const prevDom = document.getElementById(active.value.id) // 当前激活的元素
      
      if (dom) {
        const isDown = dom?.offsetTop - (prevDom?.offsetTop || 0) >= 0 // 目标元素距离当前激活目标的距离 > 0,表示向下滚动
        let prevScrollY = window.scrollY
    
        const timer = setInterval(() => {
          window.scrollBy(0, isDown ? 20 : -20) // 滚动的距离固定20(向下滚动为正,反之负)。
    
          // 如果滚动后距离 = 上次滚动的距离,说明滚动失败了,停止滚动.
          // 加这个是因为,如果页面主体的最后一块内容没有撑满屏幕时,window.scrollY的值永远都不符合与元素的offsetTop的判断,会一直死循环。
          if (prevScrollY === window.scrollY) {
            clearInterval(timer)
          }
          else {
            prevScrollY = window.scrollY // 记录当前滚动后的滚动条距离
          }
    
          if (isDown) { // 向下滾動時,頁面滾動距離大於目標元素距離時,停止滾動,反之同理
            if (prevScrollY >= dom.offsetTop - 20) { // -20用来和顶部拉开距离
              clearInterval(timer)
            }
          }
          else {
            if (prevScrollY <= dom.offsetTop) { // -20用来和顶部拉开距离
              clearInterval(timer)
            }
          }
        }, 10) // 間隔越短,滾動越流暢,滾動的準確率就越高
      }
    }
    

    如此,我们就得到了一个比较流畅滚动动画,当然时间间隔和滚动距离都可自行调整。

    值得注意的是:prevScrollY变量相关的判断一定要加,页面主体的最后一块内容没有撑满屏幕时,会导致一直滚动,无法停止。

  3. 页面滚动时,侧边栏自动定位

    页面滚动到【简介】时,侧边导航栏也应该激活对应的菜单,上面的侧边栏并没有使用a标签锚点特性和hash路由去实现,所以这一步也排除了通过监听hash路由去激活对应菜单。

    好在,有一个API很好的解决了我们当前的问题:IntersectionObserver

    IntersectionObserver可以观察元素之间的相交关系,通过这个API可以监听到到元素出现屏幕,以及出现在屏幕的比例。

    // 定义一个容器,放观察者对象
    const observers = []
    
    // Dom渲染完毕后,给侧边栏每个菜单对应的主体元素创建一个观察者
    onMounted(() => {
      menu.value.forEach((el) => {
        const observer = new IntersectionObserver(([{ intersectionRatio }]) => {
          el.intersectionRatio = intersectionRatio // 在回调中记录元素和屏幕相交的比例
        }, {
          rootMargin: '-100px', // 缩小屏幕相交的尺寸,这在有fixed定位的header和footer时非常有用
          threshold: [0.1, 0.5, 0.8, 1], // 多定义几个相交触发的时机,以便得出当前屏幕中占据最大面积的元素
        })
    
        observer.observe(document.getElementById(el.id))
        observers.push(observer)
      })
    })
    
    // 页面销毁时,注销观察者,避免不必要的开销
    onBeforeUnmount(() => {
      observers.forEach((observer) => {
        observer.disconnect()
      })
    })
    

    这段代码还可以进行优化,比如一个观察者对象可以观察多个元素,理论上只要构建一个观察者即可,而不需要new一整个数组那么多的观察者。

最后

完成侧边栏导航涉及的知识并不多,其核心知识点并不局限于vue框架,可以在任意一个框架中实现,主要知识点为:

  • IntersectionObserver 相关观察者API的使用
  • window.scrollBy 如何进行匀速滚动
  • offsetTop 和 window.scrollY 的关系和运用

完整实例可查看:码上掘金

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容