封装一个滚动组件

1,511 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

TIP 👉 苟利国家生死以,岂因祸福避趋之!____林则徐《赴戍登程口占示家人·其二》

前言

在我们日常项目开发中,我们经常会遇到一些滚动的操作,所以封装了这款滚动组件。

滚动组件

better-scroll.github.io/docs/zh-CN/

1. 属性

scrollX - 是否开启横向滚动
  • 类型:Boolean
  • 默认值:false
scrollY - 是否开启纵向滚动
  • 类型:Boolean
  • 默认值:true
scrollbar - 是否显示滚动条
  • 类型:Boolean
  • 默认值:true
mouseWheel - 是否支持鼠标滚轮
  • 类型:Boolean | Object
  • 默认值:true
  • Object类型值示例:
    {
      speed: 20,
      invert: false,
      easeTime: 300,
      discreteTime: 400,
      throttleTime: 0,
      dampingFactor: 0.1
    }
    
bounce - 是否显示回弹动画
  • 类型:Boolean | Object
  • 默认值:false
  • Object类型值示例:
    {
        top: true,
        bottom: true,
        left: true,
        right: true
      }
    
nestedScroll - 是否多层嵌套滚动条
  • 类型:Boolean | Object
  • 默认值:false
  • Object类型值示例:
    { groupId: 'dummy-divide' }
    
pullDownRefresh - 是否支持下拉刷新
  • 类型:Boolean | Object
  • 默认值:false
  • Object类型值示例:
    {
      threshold: 90,
      stop: 40
    }
    
pullUpLoad - 是否支持上拉加载
  • 类型:Boolean | Object
  • 默认值:false
  • Object类型值示例:
    {
      threshold: 0
    }
    
wrapperBgColor - 包裹器背景色(上拉、下拉漏出的底色)
  • 类型:String
  • 默认值:transparent
  • 备注:非BetterScroll 配置项
contentBgColor - 内容区域背景色(上拉、下拉漏出的底色)
  • 类型:String
  • 默认值:transparent
  • 备注:非BetterScroll 配置项
eventId - 重新初始化滚动条事件的ID
  • 类型:String
  • 无默认值
  • 备注:非BetterScroll 配置项
  • 其他组件中触发重新初始化滚动条事件
    this.$eventBus.$emit('init-scroll-' + this.scrollEventId)
    
  • 其他组件中触发刷新滚动条事件
    this.$eventBus.$emit('refresh-scroll-' + this.scrollEventId)
    
stretch - 是否拉伸内容区域的第一个子元素(将第一个子元素的min-height设置为包裹区域的高度)
  • 类型:Boolean
  • 默认值:false
  • 备注:非BetterScroll 配置项
observeDOM - 是否开启 DOM 改变的探测
  • 类型:Boolean
  • 默认值:true
observeImage - 开启图片元素加载的探测
  • 类型:Boolean
  • 默认值:false
probeType - 何时派发 scroll 事件
  • 类型:Number
  • 默认值:0
  • 说明:
    • probeType 为 0,在任何时候都不派发 scroll 事件,
    • probeType 为 1,仅仅当手指按在滚动区域上,每隔 momentumLimitTime 毫秒派发一次 scroll 事件,
    • probeType 为 2,仅仅当手指按在滚动区域上,一直派发 scroll 事件,
    • probeType 为 3,任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画
eventPassthrough - 保留原生的滚动的方向
  • 类型:String
  • 默认值:''
  • 可选值:
    • 'vertical': 保留纵向的原生滚动
    • 'horizontal': 保留横向的原生滚动
click - 点击时是否派发click事件
  • 类型:Boolean
  • 默认值:true
stopPropagation - 是否阻止事件冒泡
  • 类型:Boolean
  • 默认值:false
bounceTime - 回弹动画的动画时长(单位:ms)
  • 类型:Number
  • 默认值:800
useTransition - 是否使用 CSS3 transition 动画(如果设置为 false,则使用 requestAnimationFrame 做动画)
  • 类型:Boolean
  • 默认值:true

2. 样式要求

  • 组件外面需要包裹可以相对定位的元素,增加样式:position: relative
<template>
  <div class="scroll-parent">
    <Scroll>
      <!-- 可滚动区域内容 -->
    </Scroll>
  </div>
</template>
<script>
import Scroll from '@/components/base/scroll'
export default {
  components: {
    Scroll
  }
}
</script>
<style lang="scss" scoped>
.scroll-parent {
  position: relative;
  height: 100px;
}
</style>
  • 示例中的scroll-parent,position 为 relative,的可滚动区域的大小与该元素大小相同

一、简单示例

<template>
  <scroll :bounce="false" :observeImage="true" wrapperBgColor="#f9f9f9" contentBgColor="#fff">
    <div>
      <img src="https://tinypng.com/images/panda-confetti.png" alt=""><br>
      <img src="https://tinypng.com/images/bamboo.png" alt=""><br>
      <img src="https://tinypng.com/images/apng/panda-waving.png" alt=""><br>
      <h1>1</h1>
      <h1>2</h1>
      <h1>3</h1>
      <h1>4</h1>
      <h1>5</h1>
      <h1>6</h1>
      <h1>7</h1>
      <h1>8</h1>
      <h1>9</h1>
      <h1>10</h1>
    </div>
  </scroll>
</template>
<script>
import Scroll from '@/components/base/scroll'
export default {
  components: {
    Scroll
  }
}
</script>

二、横向滚动示例

1. 相关属性

  • scrollX - 是否支持横向滚动
  • scrollY - 是否支持纵向滚动

2. 样式要求

  • 内容必须为行内元素并且不能折行
<template>
  <div class="nav-wrap">
    <scroll :scrollbar="false" :bounce="true" :scrollX="true" :scrollY="false">
      <ul class="nav">
        <li>推荐</li>
        <li>娱乐</li>
        <li>视频</li>
        <li>生活</li>
        <li>资讯</li>
        <li>时尚</li>
        <li>美妆</li>
        <li>健康</li>
        <li>体育</li>
      </ul>
    </scroll>
  </div>
</template>
<script>
import Scroll from '@/components/base/scroll'
export default {
  name: 'ScrollHorizontal',
  components: {
    Scroll
  }
}
</script>
<style lang="scss" scoped>
$nav-height: 92px;
.nav-wrap{
  position: relative;
  height: $nav-height;
}
.nav{
  display: inline-flex;
  flex-wrap: nowrap;
  height: $nav-height;
  min-width: 750px;
  padding: 0 30px;
  background-color: #f5fffc;
  list-style: none;
  li {
    line-height: $nav-height;
    font-size: 36px;
    text-align: center;
    white-space: nowrap;
    padding: 0 40px;
  }
}
</style>

三、滚动条嵌套

1. 相关属性

  • nestedScroll - 是否多层嵌套滚动条
    • 说明:同向的嵌套需要设置此属性,非同向不需要
<template>
  <scroll :bounce="true" :observeImage="true"
          wrapperBgColor="#f9f9f9" contentBgColor="#fff" :nestedScroll="{ groupId: 'myGroup' }">
    <div class="page-content">
      <h1>1</h1>
      <h1>2</h1>
      <h1>3</h1>
      <h1>4</h1>
      <h1>5</h1>
      <div class="vertical-wrapper">
        <scroll :bounce="true" :nestedScroll="{ groupId: 'myGroup' }">
          <div class="vertical">
            <div>1</div>
            <div>2</div>
            <div>3</div>
            <div>4</div>
            <div>5</div>
            <div>6</div>
            <div>7</div>
            <div>8</div>
            <div>9</div>
            <div>10</div>
          </div>
        </scroll>
      </div>
      <h1>6</h1>
      <h1>7</h1>
      <h1>8</h1>
      <h1>9</h1>
      <h1>10</h1>
      <div class="horizontal-wrapper">
        <scroll :bounce="true" :scrollX="true" :scrollY="false">
          <div class="horizontal">
            <div>1</div>
            <div>2</div>
            <div>3</div>
            <div>4</div>
            <div>5</div>
            <div>6</div>
            <div>7</div>
            <div>8</div>
            <div>9</div>
            <div>10</div>
          </div>
        </scroll>
      </div>
      <h1>11</h1>
      <h1>12</h1>
      <h1>13</h1>
      <h1>14</h1>
      <h1>15</h1>
    </div>
  </scroll>
</template>
<script>
import Scroll from '@/components/base/scroll'
export default {
  name: 'ScrollNested',
  components: {
    Scroll
  }
}
</script>

四、刷新滚动条示例

滚动条组件嵌套多层时,当需要刷新滚动条时,可以通过全局EventBus刷新滚动条。

1. 相关属性

1) eventId

事件ID

2. 执行方式

  • 通过全局的EventBus发布重新初始化滚动条事件,事件名称为‘refresh-scroll-’ + eventId
<scroll eventId="myScroll">
    ...
</scroll>
// 其他组件触发滚动条刷新
this.$eventBus.$emit('refresh-scroll-myScroll')

五、重新初始化滚动条示例

滚动条组件嵌套多层时,当个别滚动条参数改变后需要重新初始化滚动条时,可以通过全局EventBus重新初始化滚动条。

1. 相关属性

1) eventId

事件ID

2. 执行方式

  • 通过全局的EventBus发布重新初始化滚动条事件,事件名称为‘init-scroll-’ + eventId
  • 事件参数为布尔类型:如果为true, 表示重新初始化时保持滚动条的位置
<scroll eventId="myScroll">
    ...
</scroll>
// 其他组件触发滚动条重新初始化,并保持滚动条位置不变
this.$eventBus.$emit('init-scroll-myScroll', true)

实现一个scroll.vue

<template>
  <div ref="wrapper" class="scroll-wrapper" :style="wrapperStyle" @touchmove="propagationFilter">
    <div class="scroll-content" :style="contentStyle" ref="content">
      <slot></slot>
    </div>
  </div>
</template>

<script>
import BScroll from '@better-scroll/core'
import MouseWheel from '@better-scroll/mouse-wheel'
import ScrollBar from '@better-scroll/scroll-bar'
import ObserveDOM from '@better-scroll/observe-dom'
import ObserveImage from '@better-scroll/observe-image'
import NestedScroll from '@better-scroll/nested-scroll'
import PullDown from '@better-scroll/pull-down'
import Pullup from '@better-scroll/pull-up'

BScroll.use(ScrollBar)
BScroll.use(MouseWheel)
BScroll.use(ObserveDOM)
BScroll.use(ObserveImage)
BScroll.use(NestedScroll)
BScroll.use(PullDown)
BScroll.use(Pullup)

export default {
  name: 'scroll',
  props: {
    // 是否开启横向滚动
    scrollX: {
      type: Boolean,
      default: false
    },
    // 是否开启纵向滚动
    scrollY: {
      type: Boolean,
      default: true
    },
    // 是否显示滚动条
    scrollbar: {
      type: Boolean,
      default: true
    },
    // 是否支持鼠标滚轮
    /* 对象示例:
    {
      speed: 20,
        invert: false,
      easeTime: 300,
      discreteTime: 400,
      throttleTime: 0,
      dampingFactor: 0.1
    }
    */
    mouseWheel: {
      type: [Boolean, Object],
      default: true
    },
    // 是否显示回弹动画
    /* 对象示例:
    {
      top: true,
      bottom: true,
      left: true,
      right: true
    }
    */
    bounce: {
      type: [Boolean, Object],
      default: false
    },
    // 是否多层嵌套滚动条
    /* 对象实例:
    { groupId: 'dummy-divide' }
    */
    nestedScroll: {
      type: [Boolean, Object],
      default: false
    },
    // 是否支持下拉刷新
    pullDownRefresh: {
      type: [Boolean, Object],
      default: false
    },
    // 是否支持上拉加载
    pullUpLoad: {
      type: [Boolean, Object],
      default: false
    },
    /**
     * 【非BetterScroll 配置项】包裹器背景色(上拉、下拉漏出的底色)
     */
    wrapperBgColor: {
      type: String,
      default: 'transparent'
    },
    /**
     * 【非BetterScroll 配置项】内容区域背景色
     */
    contentBgColor: {
      type: String,
      default: 'transparent'
    },
    /**
     * 【非BetterScroll 配置项】重新初始化滚动条事件的ID
     */
    eventId: {
      type: String,
      default: null
    },
    // 【非BetterScroll 配置项】是否拉伸内容区域的第一个子元素(将第一个子元素的min-height设置为包裹区域的高度)
    stretch: {
      type: Boolean,
      default: false
    },
    // 是否开启对 content 以及 content 子元素 DOM 改变的探测
    observeDOM: {
      type: Boolean,
      default: true
    },
    // 开启对 wrapper 子元素中图片元素的加载的探测
    // 【注意】:对于已经用 CSS 确定图片宽高的场景,不应该使用该插件,因为每次调用 refresh 对性能会有影响。只有在图片的宽度或者高度不确定的情况下,你才需要它。
    observeImage: {
      type: Boolean,
      default: false
    },
    /**
     * 1. probeType 为 0,在任何时候都不派发 scroll 事件,
     * 2. probeType 为 1,仅仅当手指按在滚动区域上,每隔 momentumLimitTime 毫秒派发一次 scroll 事件,
     * 3. probeType 为 2,仅仅当手指按在滚动区域上,一直派发 scroll 事件,
     * 4. probeType 为 3,任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画
     */
    probeType: {
      type: Number,
      default: 0
    },
    /**
     * 保留原生的滚动的方向,可选值:'vertical'、'horizontal'
     */
    eventPassthrough: {
      type: String,
      default: ''
    },
    /**
     * 点击时是否派发click事件
     * BetterScroll 默认会阻止浏览器的原生 click 事件。当设置为 true,BetterScroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true
     */
    click: {
      type: Boolean,
      default: true
    },
    // 是否阻止事件冒泡
    stopPropagation: {
      type: Boolean,
      default: false
    },
    // 回弹动画的动画时长(单位:ms)
    bounceTime: {
      type: Number,
      default: 800
    },
    // 是否使用 CSS3 transition 动画(如果设置为 false,则使用 requestAnimationFrame 做动画)
    useTransition: {
      type: Boolean,
      default: true
    }
  },
  data () {
    return {
      bs: null // BetterScroll 实例对象
    }
  },
  computed: {
    wrapperStyle () {
      return { backgroundColor: this.wrapperBgColor }
    },
    contentStyle () {
      let contentStyle = { backgroundColor: this.contentBgColor }
      if (this.scrollX) {
        contentStyle.display = 'inline-block'
      }
      return contentStyle
    }
  },
  created () {
    if (this.eventId) {
      this.$eventBus.$on('init-scroll-' + this.eventId, holdPosition => {
        this.initScroll(holdPosition)
      })
      this.$eventBus.$on('refresh-scroll-' + this.eventId, holdPosition => {
        this.refresh()
      })
    }
  },
  mounted () {
    this.$nextTick(() => {
      if (this.stretch) {
        this.stretchContentChild() // 拉伸内容区域的第一个子元素的高度
      }
      this.initScroll()
    })
  },
  beforeDestroy () {
    if (this.eventId) {
      this.$eventBus.$off('init-scroll-' + this.eventId)
      this.$eventBus.$off('refresh-scroll-' + this.eventId)
    }
    if (this.bs) {
      this.bs.destroy()
    }
  },
  methods: {
    /**
     * 初始化滚动条
     * holdPosition 是否保持滚动条的位置,默认为:false, 重新初始化后滚动条在顶部或左侧
     */
    initScroll (holdPosition = false) {
      // console.log('initScroll')
      // 滚动条的起始位置
      let startX = 0
      let startY = 0
      if (this.bs) {
        // 是否需要保持滚动条的位置
        if (holdPosition) {
          startX = this.bs.x
          startY = this.bs.y
        }
        // 销毁原滚动条,并解绑事件
        this.bs.destroy()
      }

      let options = {
        startX,
        startY,
        scrollX: this.scrollX, // 是否开启橫向滚动
        scrollY: this.scrollY, // 是否开启纵向滚动
        bounce: this.bounce, // 是否显示回弹动画
        probeType: this.probeType, // 何时派发 scroll 事件
        click: this.click, // 是否阻止浏览器的原生 click 事件
        stopPropagation: this.stopPropagation, // 是否阻止事件冒泡
        eventPassthrough: this.eventPassthrough // 保留某个方向的原生滚动
      }
      // bug: 插件类的选项,值为false时插件依然生效。改为值为true时才增加配置项
      // 是否显示滚动条
      if (this.scrollbar) {
        options.scrollbar = this.scrollbar
      }
      // 是否支持鼠标滚轮
      if (this.mouseWheel) {
        options.mouseWheel = (this.mouseWheel === true) ? { speed: 20, invert: false, easeTime: 300 } : this.mouseWheel
      }
      // 是否开启 DOM 改变的探测
      if (this.observeDOM) {
        options.observeDOM = this.observeDOM
      }
      // 是否开启图片加载的探测
      if (this.observeImage) {
        options.observeImage = this.observeImage
      }
      // 是否嵌套滚动条
      if (this.nestedScroll) {
        options.nestedScroll = this.nestedScroll
      }
      // 是否支持下拉刷新
      if (this.pullDownRefresh) {
        options.pullDownRefresh = this.pullDownRefresh
      }
      // 是否支持上拉加载
      if (this.pullUpLoad) {
        options.pullUpLoad = this.pullUpLoad
      }

      console.log('滚动条初始化配置:', options)
      // 重新创建BetterScroll
      const bs = new BScroll(this.$refs.wrapper, options)
      this.bs = bs
      this.$emit('created', bs)
      this.bs.hooks.on('refresh', () => { console.log('滚动条刷新 ' + new Date()) })
    },
    disable () {
      this.bs && this.bs.disable()
    },
    enable () {
      this.bs && this.bs.enable()
    },
    refresh () {
      this.bs && this.bs.refresh()
    },
    // 拉伸内容区域(内容区域的子元素的最小高度设为包裹区域的高度)
    stretchContentChild () {
      let wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height
      let children = this.$refs.content.children
      if (wrapperHeight && children && children.length === 1) {
        let child = children[0]
        child.style.minHeight = wrapperHeight + 'px'
      }
    },
    // 事件冒泡过滤
    propagationFilter (e) {
      if (e.target && e.target.tagName === 'TEXTAREA') { // textarea 阻止冒泡
        e.stopPropagation()
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.scroll-wrapper {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  overflow: hidden;
}
.scroll-content {
  min-height: 100%;
  &:before {
    content: '';
    display: table;
  }
  &:after {
    content: '';
    display: table;
    clear: both;
  }
}
.pulldown-wrapper {
  position: absolute;
  width: 100%;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: all;
}
.pullup-wrapper {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px 0;
}
.pulldown {
  position: absolute;
  top: 0;
  left: 0;
}
.pulldown-enter-active {
  transition: all 0.2s;
}
.pulldown-enter, .pulldown-leave-active {
  transform: translateY(-100%);
  transition: all 0.2s;
}
.tip-msg {
  position: absolute;
  width: 100%;
  text-align: center;
}
::v-deep .bscroll-vertical-scrollbar{
  width: 10px !important;
}
::v-deep .bscroll-horizontal-scrollbar {
  height: 10px !important;
}
</style>

index.js

/**
 * 滚动条组件
 * @see https://github.com/ustbhuangyi/better-scroll
 * @see https://better-scroll.github.io/docs/zh-CN/
 */
import Scroll from './Scroll.vue'
export default Scroll

感谢评论区大佬的点拨。

希望看完的朋友可以给个赞,鼓励一下