开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
侧边栏导航也称电梯导航,通常悬浮在页面侧边作为页面目录进行使用。
主要功能
主要实现的功能有两点:
- 点击侧边栏导航滚动到页面指定位置
- 页面滚动到指定位置时,侧边栏自动定位
提示
案例基于vue3项目实现,但其核心原理和所使用框架并无太大关联,套用到任意一个框架中都适用。
开始实现:
-
页面布局
搭建一个简单的页面案例,左侧是滚动浏览内容,右侧是侧边导航栏。
滚动内容需要和侧边栏菜单进行联动,给主题内容块绑定一个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; } } }
得到页面结构如下:
-
点击侧边栏滚动到页面指定目录
这一步骤最简单的方法是通过 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变量相关的判断一定要加,页面主体的最后一块内容没有撑满屏幕时,会导致一直滚动,无法停止。
-
页面滚动时,侧边栏自动定位
页面滚动到【简介】时,侧边导航栏也应该激活对应的菜单,上面的侧边栏并没有使用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 的关系和运用
完整实例可查看:码上掘金
暂无评论内容