vue-滚动吸顶之sticky组件

919

在我们日常开发业务中,时常会遇到一些滚动吸顶的交互; css有提供position:sticky属性,但是该属性使用限制多,并且最大的问题,就是在安卓机上的表现简直惨不忍睹。 鉴于sticky其实就是fixed与relative的结合(当修饰的目标节点再屏幕中时表现为relative,当要超出的时候是fixed的形式展现),所以我们完全可以使用js来实现这一功能。

<template lang="pug">
  .sticky-wrap(:style="compsRootStyle")
      .sticky-change-wrap(:class="className" :style="stickyStyle")
          slot
          	| this sticky content
</template>

sticky-change-wrap,根据滚动情况,做fixed或者非fixed变化,fixed中会脱离文档流,造成后续元素陡然上移,或者非fixed时,下面内容陡然被挤下去。sticky-wrap盒子外层,需设置高,就是解决上述变化抖动的问题;

export default {
  name: 'Sticky',
  props: {
    stickyTop: {
      type: Number,
      default: 0
    },
    zIndex: {
      type: Number,
      default: 1
    },
    className: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      position: '',
      width: '',
      isSticky: false,
      compsRootStyle: {}
    }
  },
  computed: {
    stickyStyle() {
      const { position, width, stickyTop, isSticky } = this
      return {
        width
        position,
        top: isSticky ? `${stickyTop}px` : '',
        ...this.compsRootStyle
      }
    }
  },
  mounted() {
    this.compsRootStyle = { height: this.$el.getBoundingClientRect().height, zIndex: this.zIndex }
    window.addEventListener('scroll', this.handleScroll)
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('scroll', this.handleScroll)
    })
  },
  
  methods: {
    handleScroll() {
      const width = this.$el.getBoundingClientRect().width
      this.width = width || 'auto'
      const offsetTop = this.$el.getBoundingClientRect().top
      offsetTop < this.stickyTop ? this.setSticky() : this.setNotSticky()
    },

    setSticky() {
      if (this.isSticky) return
      this.isSticky = true
      this.position = 'fixed'
      this.width = this.width + 'px'
    },

    setNotSticky() {
      if (!this.isSticky) return
      this.isSticky = false
      this.position = ''
      this.width = 'auto'
    }
  }
}

进一步完善

上述代码已经很好的实现了滚动到目标top时吸附,滚出该目标时随内容滚动的效果; 但是在pc端有一个bug,就是在fixed定位之后,其宽度将不再和父元素的保持一致。这时会出现,改变屏幕大小,宽度并不发变化的问题;解决方案: 加一个resize监听

export default {
  mounted() {
    window.addEventListener('resize', this.handleResize)
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.handleResize)
    })
  },

  methods: {
    handleResize() {
      if (this.isSticky) {
        this.width = this.$el.getBoundingClientRect().width + 'px'
      }
    },
  }
}
</script>

在组件keep-alive中,首次进入会触发mounted,但是切换其他组件后再返回不会再执行mounted等生命周期函数,这是因为组件已经被缓存了。

唯一会执行的是activated。

使用

sticky(:sticky-top="0")
  .item(v-for="item in 100") placeholder