记录一次 vue 吸顶组件的实现历程

1,085 阅读2分钟

所有代码

<!-- 
吸顶组件 
用法: <StickTop>
        <div>李白</div>
    </StickTop>

    <StickTop :offsetTop="30">
        <div>李白</div>
    </StickTop>
    属性
        offsetTop Number类型    距离顶部的距离(px)  默认为0
-->

<template>
    <div>
        <div ref="point" style="z-index: 2;" :style="fixedStyle">
            <slot></slot>
        </div>
        <div v-show="affix" :style="placeholderStyle"></div>
    </div>
</template>
<script>
/*
    在滚动过程中计算获取元素在没有fixed定位的时候的距离顶部的距离 (iview是这么做的)
 */
function getOffset(element) {
    const rect = element.getBoundingClientRect()
    // 这两行代码不兼容IE (兼容IE的方法是 document.documentElement.scrollTop / scrollLeft)
    const scrollTop = window.pageYOffset
    const scrollLeft = window.pageXOffset
    /*
    相加后的结果是 初始位置 在滚动过程中他将是一个固定值
    */
    return {
        top: rect.top + scrollTop,
        left: rect.left + scrollLeft
    }
}

export default {
    props: {
        offsetTop: {
            type: Number,
            default: 0
        }
    },
    data() {
        return {
            affix: false, // 吸顶状态
            fixedStyle: {}, // 吸顶元素的样式
            placeholderStyle: {} // 占位容器
        }
    },
    methods: {
        handleScroll() {
            // getOffset方法是从iview抄来的
            // https://gitee.com/view-design/ViewUI/blob/master/src/components/affix/affix.vue
            let rect = getOffset(this.$el) // 根元素距离顶部和左侧的距离 主要是想获取初始值
            if (rect.top - this.offsetTop < window.pageYOffset) {
                this.fixedStyle = {
                    position: 'fixed',
                    top: `${this.offsetTop}px`,
                    width: `${this.$el.offsetWidth}px`
                }
                this.placeholderStyle = { height: this.$refs.point.clientHeight + 'px' }
                this.affix = true
            } else {
                this.fixedStyle = null
                this.placeholderStyle = {}
                this.affix = false
            }
        }
    },
    mounted() {
        addEventListener('scroll', this.handleScroll)
    },
    beforeDestroy() {
        removeEventListener('scroll', this.handleScroll)
    }
}
</script>

最初的想法

模板篇

  • 组件内部有个插槽,插槽里的内容就是需要吸顶的元素
  • 组件内还应有个占位的容器,容器高度为吸顶的元素的高度

思想篇

没有思想的时候创造思想, 直接打开已有的效果查看,

分别查看了 iview 和 antd

iview

antd

一个叫'图钉',一个叫 '固钉',, 还是固钉比较贴切(请忽略我起的名字............)

  • 在滚动浏览器滚动条的时候通过F12检查元素发现,当目标元素(希望固定的元素)到达期望位置(距离顶部的距离)时,会向目标元素的父元素加上 fixed 定位
  • 在滚动过程中控制 slot 的父元素的css属性即可,即 组件内的data中的 fixedStyle
  • 顺便定义一下吸顶状态 affix (变量命名参考了 antd 和 iview )
  • placeholderStyle 控制当元素吸顶后占位容器的样式

开始实现

监听滚动

参考了 iview 的源码, 直接在 mounted 中监听 scroll 事件, 在 beforeDestroy 中取消监听

期初我在 mounted 中获取元素的初始位置,在自己测试的时候发现,当已经处于 fixed 定位的时候,刷新浏览器,会产生奇怪的 bug (就不描述了),虽然经过一番测试后,在 mounted中监听事件执行前加上了 window.scrollTop({ top: 0 }), 能够解决这个问题,但是解决方式太诡异了,本人受不了...

最后还是参考源码,在滚动过程中实时获取元素位置 就是 getOffset 函数。element.getBoundingClientRect() 函数可以获取元素的宽高距离等属性,下图

顺便提一下--来自MDN: 如果是标准盒子模型,元素的尺寸等于width/height + padding + border-width的总和。如果box-sizing: border-box,元素的的尺寸等于 width/height。

这里以使用 top 举例, getOffset()函数就干了一件事,获取目标元素在浏览器滚动过程中距离文档顶部的距离(不是浏览器顶部), 再说一遍 目标元素距离文档顶部的位置

接下来一切的逻辑都在 handleScroll 函数里,(函数命名也参考了源码)

通过比较目标元素的目标位置与浏览器滚动条滚动过的距离,设置样式即可

彩蛋

最开始的时候没有使用 addEventListener,而使用了window.onscroll = () => {} 导致同时使用两次的时候,由于 window.onscroll 被复写而出现 bug , 后来我终于步入了正轨......