介绍 Better-Scroll 组件的使用
声明:本文的代码取自Better-Scroll 的 GitHub 仓库中。
仓库地址:https://github.com/ustbhuangyi/better-scroll
事例代码地址:https://github.com/ustbhuangyi/better-scroll/blob/master/example/components/scroll/scroll.vue
本文描述主要针对移动端 Vue 项目的解决方案!
描述:这是一篇介绍移动端使用 Better-Scroll 库的文章。这里将会介绍自己在工作中使用该组件的封装时背后的艺术,应用场景,解决的问题。
一、我们为什么要使用 Better-Scroll 做局部滚动?
我想,当你找到这片文章的时候,就说明你可能已经遇到了一些问题比如:在页面上某些东西(可能是顶部的Header,或是底部的导航栏,或者两者都有)需要固定不动,而其他一些需要滚动(主要是中间的内容列表,不上不下,很尴尬);滚动的时候不出现滚动条(虽然有隐藏滚动条的CSS属性,但是兼容性差;隐藏后,不同平台表现差异比较大);又比如做一些,左右联动的组件实现,最典型的左右联动比如城市的选择,和右边英文字母的缩写;等等很多应用场景。
有没有以上列举的场景,感觉可以用原生的JS,HTML来写,但是写起来,感觉好像并没有想的这么简单…
所以这里就是使用 Better-Scroll 库的意义。在实际开大的时候,用别人的轮子感觉就是一个比吃饭还平常的东西,自己捣鼓,做Demo可以,做产品不行!
再加上,这是一个成熟的JS库,目前(2019/01/05)Github 上的star:
还有完善的文档,给个链接:Better-Scroll中文文档,大家自己撸。文档里面有很多Demo的链接地址,大家可以进去看看。在国内有时候进不去,需要从GitHub那边进去,自取之!
二、使用前的碎碎念
这个关于怎么在 Vue 项目怎能引入这个组件,怎么初始化 Vue 项目等这些问题,在这里不统一说明,这应该是基本功,如果不会,请自行去撸官方文档,哪个不会,看哪个。
还有接下来要说的内容在领域里面感觉比较狭窄:就是用 Better-Scroll 解决移动端滚动,上拉刷新,下拉加载问题,并且封装成组件。
假设你有一定的 Vue 移动端开发经验,遇到了一些问题,并且开始尝试解决的场景,最好用过 Better-Scroll 的。
接下来基本功的东西不多说,直接开始讲解 事例代码地址 里面的代码!
为了讲解方便,直接把代码Copy过了,先看一眼:
<template>
<div ref="wrapper" class="list-wrapper">
<div class="scroll-content">
<div ref="listWrapper">
<slot>
<ul class="list-content">
<li @click="clickItem($event,item)" class="list-item" v-for="item in data">{{item}}</li>
</ul>
</slot>
</div>
<slot name="pullup"
:pullUpLoad="pullUpLoad"
:isPullUpLoad="isPullUpLoad"
>
<div class="pullup-wrapper" v-if="pullUpLoad">
<div class="before-trigger" v-if="!isPullUpLoad">
<span>{{pullUpTxt}}</span>
</div>
<div class="after-trigger" v-else>
<loading></loading>
</div>
</div>
</slot>
</div>
<slot name="pulldown"
:pullDownRefresh="pullDownRefresh"
:pullDownStyle="pullDownStyle"
:beforePullDown="beforePullDown"
:isPullingDown="isPullingDown"
:bubbleY="bubbleY"
>
<div ref="pulldown" class="pulldown-wrapper" :style="pullDownStyle" v-if="pullDownRefresh">
<div class="before-trigger" v-if="beforePullDown">
<bubble :y="bubbleY"></bubble>
</div>
<div class="after-trigger" v-else>
<div v-if="isPullingDown" class="loading">
<loading></loading>
</div>
<div v-else><span>{{refreshTxt}}</span></div>
</div>
</div>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
import BScroll from '../../../src/index'
import Loading from '../loading/loading.vue'
import Bubble from '../bubble/bubble.vue'
import { getRect } from '../../common/js/dom'
const COMPONENT_NAME = 'scroll'
const DIRECTION_H = 'horizontal'
const DIRECTION_V = 'vertical'
export default {
name: COMPONENT_NAME,
props: {
data: {
type: Array,
default: function () {
return []
}
},
probeType: {
type: Number,
default: 1
},
click: {
type: Boolean,
default: true
},
listenScroll: {
type: Boolean,
default: false
},
listenBeforeScroll: {
type: Boolean,
default: false
},
listenScrollEnd: {
type: Boolean,
default: false
},
direction: {
type: String,
default: DIRECTION_V
},
scrollbar: {
type: null,
default: false
},
pullDownRefresh: {
type: null,
default: false
},
pullUpLoad: {
type: null,
default: false
},
startY: {
type: Number,
default: 0
},
refreshDelay: {
type: Number,
default: 20
},
freeScroll: {
type: Boolean,
default: false
},
mouseWheel: {
type: Boolean,
default: false
},
bounce: {
default: true
},
zoom: {
default: false
}
},
data() {
return {
beforePullDown: true,
isRebounding: false,
isPullingDown: false,
isPullUpLoad: false,
pullUpDirty: true,
pullDownStyle: '',
bubbleY: 0
}
},
computed: {
pullUpTxt() {
const moreTxt = (this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.more) || this.$i18n.t('scrollComponent.defaultLoadTxtMore')
const noMoreTxt = (this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.noMore) || this.$i18n.t('scrollComponent.defaultLoadTxtNoMore')
return this.pullUpDirty ? moreTxt : noMoreTxt
},
refreshTxt() {
return (this.pullDownRefresh && this.pullDownRefresh.txt) || this.$i18n.t('scrollComponent.defaultRefreshTxt')
}
},
created() {
this.pullDownInitTop = -50
},
mounted() {
setTimeout(() => {
this.initScroll()
}, 20)
},
destroyed() {
this.$refs.scroll && this.$refs.scroll.destroy()
},
methods: {
initScroll() {
if (!this.$refs.wrapper) {
return
}
if (this.$refs.listWrapper && (this.pullDownRefresh || this.pullUpLoad)) {
this.$refs.listWrapper.style.minHeight = `${getRect(this.$refs.wrapper).height + 1}px`
}
let options = {
probeType: this.probeType,
click: this.click,
scrollY: this.freeScroll || this.direction === DIRECTION_V,
scrollX: this.freeScroll || this.direction === DIRECTION_H,
scrollbar: this.scrollbar,
pullDownRefresh: this.pullDownRefresh,
pullUpLoad: this.pullUpLoad,
startY: this.startY,
freeScroll: this.freeScroll,
mouseWheel: this.mouseWheel,
bounce: this.bounce,
zoom: this.zoom
}
this.scroll = new BScroll(this.$refs.wrapper, options)
if (this.listenScroll) {
this.scroll.on('scroll', (pos) => {
this.$emit('scroll', pos)
})
}
if (this.listenScrollEnd) {
this.scroll.on('scrollEnd', (pos) => {
this.$emit('scroll-end', pos)
})
}
if (this.listenBeforeScroll) {
this.scroll.on('beforeScrollStart', () => {
this.$emit('beforeScrollStart')
})
this.scroll.on('scrollStart', () => {
this.$emit('scroll-start')
})
}
if (this.pullDownRefresh) {
this._initPullDownRefresh()
}
if (this.pullUpLoad) {
this._initPullUpLoad()
}
},
disable() {
this.scroll && this.scroll.disable()
},
enable() {
this.scroll && this.scroll.enable()
},
refresh() {
this.scroll && this.scroll.refresh()
},
scrollTo() {
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
},
scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
},
clickItem(e, item) {
console.log(e)
this.$emit('click', item)
},
destroy() {
this.scroll.destroy()
},
forceUpdate(dirty) {
if (this.pullDownRefresh && this.isPullingDown) {
this.isPullingDown = false
this._reboundPullDown().then(() => {
this._afterPullDown()
})
} else if (this.pullUpLoad && this.isPullUpLoad) {
this.isPullUpLoad = false
this.scroll.finishPullUp()
this.pullUpDirty = dirty
this.refresh()
} else {
this.refresh()
}
},
_initPullDownRefresh() {
this.scroll.on('pullingDown', () => {
this.beforePullDown = false
this.isPullingDown = true
this.$emit('pullingDown')
})
this.scroll.on('scroll', (pos) => {
if (!this.pullDownRefresh) {
return
}
if (this.beforePullDown) {
this.bubbleY = Math.max(0, pos.y + this.pullDownInitTop)
this.pullDownStyle = `top:${Math.min(pos.y + this.pullDownInitTop, 10)}px`
} else {
this.bubbleY = 0
}
if (this.isRebounding) {
this.pullDownStyle = `top:${10 - (this.pullDownRefresh.stop - pos.y)}px`
}
})
},
_initPullUpLoad() {
this.scroll.on('pullingUp', () => {
this.isPullUpLoad = true
this.$emit('pullingUp')
})
},
_reboundPullDown() {
const {stopTime = 600} = this.pullDownRefresh
return new Promise((resolve) => {
setTimeout(() => {
this.isRebounding = true
this.scroll.finishPullDown()
resolve()
}, stopTime)
})
},
_afterPullDown() {
setTimeout(() => {
this.pullDownStyle = `top:${this.pullDownInitTop}px`
this.beforePullDown = true
this.isRebounding = false
this.refresh()
}, this.scroll.options.bounceTime)
}
},
watch: {
data() {
setTimeout(() => {
this.forceUpdate(true)
}, this.refreshDelay)
}
},
components: {
Loading,
Bubble
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.list-wrapper
position: relative
height: 100%
/*position: absolute*/
/*left: 0*/
/*top: 0*/
/*right: 0*/
/*bottom: 0*/
overflow: hidden
background: #fff
.scroll-content
position: relative
z-index: 1
.list-content
position: relative
z-index: 10
background: #fff
.list-item
height: 60px
line-height: 60px
font-size: 18px
padding-left: 20px
border-bottom: 1px solid #e5e5e5
.pulldown-wrapper
position: absolute
width: 100%
left: 0
display: flex
justify-content center
align-items center
transition: all
.after-trigger
margin-top: 10px
.pullup-wrapper
width: 100%
display: flex
justify-content center
align-items center
padding: 16px 0
</style>
四、组件封装的艺术
这部分我主要会说说这个组件是怎么封装成功的。当然封装的方法有千千万。这里我更想说是作者为什么要这么封装?更好扩展?有什么特殊的用意,达到的目的?以及以后我们要需要封装自己的东西的时候怎么想的!总之:重在思路!
(1)初始化 Better-Scroll 组件。如果你看过文档就知道,需要找到一个 Dom 节点挂载需要滚动的内容,并且需要用 New 的方式实例化组件。
这里主要看到 methods 的方法钩子中,第一个 initScroll() 方法就是组件的入口点。通过 this.$refs.wrapper 的方式获取一个 Dom 节点,这个方法是 Vue 提供的。然后根据用户在父组件传递过来的 Props 属性来实例化 Scroll 组件应该有的功能,看图中的注解:
心得:这里可以说说 Vue 的父子组件交互的方式:
父组件向子组件传递数据用:属性。父组件传,组件通过props定义并接受数据;
子组件向父组件交互用:事件。子组件通过:this.$emit(‘eventName’, data) 的方式触发事件,父组件只要监听事件即可。所以以后要是自己封装的话,可以通过外界传递参数进来,通过不同的参数,实例化不同的功能,感觉不失为组件封装的一个好办法!
(2)使用 Watch 实现自动化。怎么说呢,这个属性感觉可能很多人都知道,并且在有些场景还被滥用,很多地方明明可以使用其他简单的方式实现,可还是选择了 Watch。任何东西设计出来都有他的应用场景,我们不应不分青红皂白,乱用。
这个属性我个人觉得在:自动化,监听异步 方面是最实用的。结合例子看:
看到下面的 Watch 属性:
因为 better-scroll 组件自身不能根据内容的高度,动态撑开页面,需要手动调用一个:refresh() 方法。当你自己封装组件的时候,只要页面的数据变化,自己手动调用这个方法刷新页面高度,是完全没有问题的。但是感觉这样比较麻烦,并且容易造成疏漏。当别人接手你的代码的时候,忘了调用,页面就会不正常了。所以有没有一种:当页面的数据变化的时候,实现自动刷新高度呢?
答案就是使用 watch 属性,我只要监听 data 的数据变化,调用相应的刷新方法即可。这就是我想表达的自动化的概念,用户不需要知道是怎么做的,只要传过来一个数据,当数据变化的时候,底层去实现相应的逻辑。同样用到这样的实现的还有在下拉刷新的时候的一个 canvas 画图的时候:
代码链接:https://github.com/ustbhuangyi/better-scroll/blob/master/example/components/bubble/bubble.vue
其实就是页面使用的:Bubble 组件,大家可以去看看他里面的代码,最能提现前面说的那种实现思路。
(3)上拉刷新,下拉加载的实现。感觉这个应用场景很常用,但是自己写有一堆问题的那种。还好这个插件库贴心实现了这个功能,我们只要配置一下,然后监听一下对应的功能就好。首先来看看官方实现的效果:https://ustbhuangyi.github.io/better-scroll/#/examples/vertical-scroll/zh
在代码里面,作者已经写好这里面的交互了,包括提示文字,什么的,你只要配置一下就能用:
已经在项目中使用,直接配置打开,然后监听相应的事件,然后做处理,就可以简单拥有一个强大的功能!
(4)其他。这里感觉还有一些零散的功能点,可以说说,比如:滚动到某一个元素,滚动到页面的某一个坐标,禁用/启用页面,销毁组件等功能比较简单,就不再讲解。
五、结束语
感觉写到这里,我最想说的组件的酷炫的功能就已经说完了。感觉这个是一个很神奇的JS,感觉他的Api定制的特别合理,很详细,很所业务场景感觉都有对应的方法,用起来还是很爽的。好东西大家一起分享,哈哈哈…
以上纯属一些个人的见解,如有描述不合理地方,欢迎讨论!
发版:2019.01.05