近期产品小哥哥给我提了一个新需求,在一个页面的滚动区中添加一组锚点定位按钮,点击按钮将对应的元素显示在页面的可视区中。当按钮组超出页面可视区的时候将其固定在滚动区域的头部,当滚动区滚动时,高亮距离滚动区顶部最近的元素所匹配的锚点按钮。
拆分功能点
现在我们已经明确需求了,接下来我们总结一下这个需求有哪些功能点:
按钮组要有吸顶效果
点击按钮要有锚点定位功能
滚动内容区需要找到对应的按钮并高亮
吸顶组件
要做一个吸顶效果最简单的方式是将 css 的 position 属性设置为 sticky, 这样就实现粘性布局。
.sticky-container { position: sticky; top: 0px; }
上面的示例仅仅用了两行 css 的代码就实现了粘性布局,但由于 IE 浏览器完全不支持粘性布局,而我的项目又需要支持一部分的 IE 浏览器,所以就需要手动去实现这样一个功能。
MDN 官方对粘性布局的解释是这样的,粘性布局元素默认是相对定位的,当粘性元素超出父元素的指定值(如 `top` 、`left` 等),例如上面的示例,当元素粘性元素改为固定定位。关于父级元素 MDN 描述的不是很精确,这里的父级元素指的是父级滚动元素,如果没有父级滚动元素则将 `body` 元素作为父级元素。
既然需要自己实现一个吸顶的效果,思考到其他页面可能也会使用的吸顶的功能,所以决定将其单独抽离成一个通用组件。首先我们知道粘性布局是对父级滚动元素定位,所以我们要先找到父级滚动元素,这个功能我们可以通过两种方式实现,一种是向上查找,一种是通过 props 传递一个唯一标识的 css 选择器。
我觉得其他项目可能也会遇到这个功能,所以我定义组件 尽量向着开源靠拢,所以我这里同时支持两种方案。首先我们要实现一个查找父级滚动元素的功能,如何判断一个元素是滚动元素呢?很简单判断其 `overflow` 是否是 `auto` 或者 `scroll`。
// util.js 文件 // 判断一个元素是否是滚动元素 const scrollList = ['auto', 'scroll'] export function hasScrollElement(el, direction = 'vertical') { if (!el) return const style = window.getComputedStyle(el) if (direction === 'vertical') { return scrollList.includes(style.overflowY) } else if (direction === 'horizontal') { return scrollList.includes(style.overflowX) } } // 获取第一个滚动元素 export function getFirstScrollElement(el, direction = 'vertical') { if (!el) return if (hasScrollElement(el, direction)) { return el } else { return getFirstScrollElement(el && el.parentElement, direction) } }
这里说下实现吸顶效果所需要的一些基础知识:
fixed 定位是相对于浏览器的可视区进行定位,这意味着即使页面滚动,它还是会固定在相同的位置
offsetTop 是一个只读的属性,它返回当前元素相对于距离它最近的父级定位元素顶部的距离。
scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素值,`scrollTop` 表示这个元素达到父级滚动元素顶部的距离。
<template> <div :class="fixedClass" :style="{ top: top + 'px', zIndex }"> <slot></slot> </div> </template> <script> export default { props: { top: Number, parent: String, zIndex: Number }, data() { return { fixedClass: '', scrollElement: null } }, mounted() { this.initScrollElement() }, destroyed() { this.removeScrollEvent() }, methods: { handleScroll() { const scrollOffsetTop = this.$el.offsetTop - this.top if (this.scrollElement.scrollTop >= scrollOffsetTop) { this.fixedClass = 'top-fixed' } else { this.fixedClass = '' } }, initScrollElement() { const element = document.querySelector(this.parent) if (element) { this.removeScrollEvent() this.scrollElement = element this.scrollElement.addEventListener('scroll', this.handleScroll) } }, removeScrollEvent() { if (this.scrollElement) { this.scrollElement.removeEventListener('scroll', this.handleScroll) } } } } </script> <style lang="scss"> .cpt-sticky { .top-fixed { position: fixed; width: 100%; background: #fff; } } </style>
就像上面的示例代码一样,短短几十行就实现了一个吸顶组件,不过它实现了吸顶的功能,但是还有一些缺陷。
在慢速滚动页面,吸顶组件在固定与非固定的时候有明显的卡顿现象。