小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
TIP 👉 苟利国家生死以,岂因祸福避趋之!____林则徐《赴戍登程口占示家人·其二》
前言
在我们日常项目开发中,我们经常会遇到一些滚动的操作,所以封装了这款滚动组件。滚动组件
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
感谢评论区大佬的点拨。