官網(wǎng):http://ustbhuangyi.github.io/better-scroll/doc/?q=#
GitHub:https://github.com/ustbhuangyi/better-scroll
在我們?nèi)粘5囊苿?dòng)端項(xiàng)目開發(fā)中煞额,處理滾動(dòng)列表是再常見不過(guò)的需求了况增,可以是豎向滾動(dòng)的列表偷溺,也可以是橫向的窖剑,用
better-scroll
可以幫助我們實(shí)現(xiàn)這個(gè)
什么是 better-scroll
better-scroll
是一個(gè)移動(dòng)端滾動(dòng)的解決方案谈飒,它是基于iscroll
的重寫涛癌,它和iscroll
的主要區(qū)別在這里翼虫。better-scroll
也很強(qiáng)大仲智,不僅可以做普通的滾動(dòng)列表饼齿,還可以做輪播圖饲漾、picker 等等。
better-scroll的滾動(dòng)原理
不少人可能用過(guò) better-scroll缕溉,出現(xiàn)最多的問(wèn)題是:
- 我的
better-scroll
初始化了考传, 但是沒(méi)法滾動(dòng)。 - 不能滾動(dòng)是現(xiàn)象证鸥,我們得搞清楚這其中的根本原因僚楞。在這之前,我們先來(lái)看一下瀏覽器的滾動(dòng)原理:
- 瀏覽器的滾動(dòng)條大家都會(huì)遇到枉层,當(dāng)頁(yè)面內(nèi)容的高度超過(guò)視口高度的時(shí)候泉褐,會(huì)出現(xiàn)縱向滾動(dòng)條;當(dāng)頁(yè)面內(nèi)容的寬度超過(guò)視口寬度的時(shí)候鸟蜡,會(huì)出現(xiàn)橫向滾動(dòng)條膜赃。也就是當(dāng)我們的視口展示不下內(nèi)容的時(shí)候,會(huì)通過(guò)滾動(dòng)條的方式讓用戶滾動(dòng)屏幕看到剩余的內(nèi)容揉忘。
那么對(duì)于 better-scroll 也是一樣的道理跳座,我們先來(lái)看一下 better-scroll 常見的 html 結(jié)構(gòu):
<div class="wrapper">
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
</div>
為了更加直觀,我們?cè)賮?lái)看一張圖:
- 綠色部分為
wrapper
癌淮,也就是父容器躺坟,它會(huì)有固定的高度。 - 黃色部分為
content
乳蓄,它是父容器的第一個(gè)子元素咪橙,它的高度會(huì)隨著內(nèi)容的大小而撐高。那么,當(dāng)content
的高度不超過(guò)父容器的高度美侦,是不能滾動(dòng)的产舞,而它一旦超過(guò)了父容器的高度,我們就可以滾動(dòng)內(nèi)容區(qū)了菠剩,這就是better-scroll
的滾動(dòng)原理易猫。
那么,我們?cè)趺闯跏蓟?better-scroll 呢具壮,如果是上述 html 結(jié)構(gòu)准颓,那么初始化代碼如下:
import BScroll from 'better-scroll'
let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper, {})
better-scroll 對(duì)外暴露了一個(gè) BScroll 的類,我們初始化只需要 new 一個(gè)類的實(shí)例即可棺妓。第一個(gè)參數(shù)就是我們 wrapper 的 DOM 對(duì)象攘已,第二個(gè)是一些配置參數(shù),具體參考 better-scroll 的文檔怜跑。
-
better-scroll
的初始化時(shí)機(jī)很重要样勃,因?yàn)樗诔跏蓟臅r(shí)候,會(huì)計(jì)算父元素和子元素的高度和寬度性芬,來(lái)決定是否可以縱向和橫向滾動(dòng)峡眶。因此,我們?cè)诔跏蓟臅r(shí)候植锉,必須確保父元素和子元素的內(nèi)容已經(jīng)正確渲染了辫樱。如果子元素或者父元素 DOM 結(jié)構(gòu)發(fā)生改變的時(shí)候,必須重新調(diào)用scroll.refresh()
方法重新計(jì)算來(lái)確保滾動(dòng)效果的正常汽煮。 - 所以很多反饋的
better-scroll
不能滾動(dòng)的原因多半是初始化better-scroll
的時(shí)機(jī)不對(duì)搏熄,或者是當(dāng)DOM
結(jié)構(gòu)發(fā)送變化的時(shí)候并沒(méi)有重新計(jì)算better-scroll
。
better-scroll 遇見 Vue
相信很多人對(duì) Vue.js 都不陌生暇赤,當(dāng) better-scroll 遇見 Vue,會(huì)擦出怎樣的火花呢宵凌?
如何在 Vue 中使用 better-scroll
我們把 better-scroll 和 Vue 做了結(jié)合鞋囊,實(shí)現(xiàn)了很多列表滾動(dòng)的效果。在 Vue 中的使用方法如下:
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li>...</li>
...
</ul>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
mounted() {
this.$nextTick(() => {
this.scroll = new Bscroll(this.$refs.wrapper, {})
})
}
}
</script>
Vue.js 提供了我們一個(gè)獲取 DOM 對(duì)象的接口—— vm.$refs
瞎惫。在這里溜腐,我們通過(guò)了 this.$refs.wrapper
訪問(wèn)到了這個(gè) DOM 對(duì)象,并且我們?cè)?mounted 這個(gè)鉤子函數(shù)里瓜喇,this.$nextTick
的回調(diào)函數(shù)中初始化 better-scroll 挺益。因?yàn)檫@個(gè)時(shí)候,wrapper 的 DOM 已經(jīng)渲染了乘寒,我們可以正確計(jì)算它以及它內(nèi)層 content 的高度望众,以確保滾動(dòng)正常。
這里的 this.$nextTick
是一個(gè)異步函數(shù),為了確保 DOM 已經(jīng)渲染烂翰,感興趣的同學(xué)可以了解一下它的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)夯缺,底層用到了 MutationObserver 或者是 setTimeout(fn, 0)
。其實(shí)我們?cè)谶@里把 this.$nextTick
替換成 setTimeout(fn, 20)
也是可以的(20 ms 是一個(gè)經(jīng)驗(yàn)值甘耿,每一個(gè) Tick 約為 17 ms)踊兜,對(duì)用戶體驗(yàn)而言都是無(wú)感知的。
異步數(shù)據(jù)的處理
在我們的實(shí)際工作中佳恬,列表的數(shù)據(jù)往往都是異步獲取的捏境,因此我們初始化 better-scroll
的時(shí)機(jī)需要在數(shù)據(jù)獲取后,代碼如下:
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
data() {
return {
data: []
}
},
created() {
requestData().then((res) => {
this.data = res.data
this.$nextTick(() => {
this.scroll = new Bscroll(this.$refs.wrapper, {})
})
})
}
}
</script>
這里的 requestData 是偽代碼毁葱,作用就是發(fā)起一個(gè) http 請(qǐng)求從服務(wù)端獲取數(shù)據(jù)垫言,并且這個(gè)函數(shù)返回的是一個(gè) promise(實(shí)際項(xiàng)目中我們可能會(huì)用 axios 或者 vue-resource)。我們獲取到數(shù)據(jù)的后头谜,需要通過(guò)異步的方式再去初始化 better-scroll骏掀,因?yàn)?Vue 是數(shù)據(jù)驅(qū)動(dòng)的, Vue 數(shù)據(jù)發(fā)生變化(this.data = res.data
)到頁(yè)面重新渲染是一個(gè)異步的過(guò)程柱告,我們的初始化時(shí)機(jī)是要在 DOM 重新渲染后截驮,所以這里用到了 this.$nextTick
,當(dāng)然替換成 setTimeout(fn, 20)
也是可以的际度。
為什么這里在 created 這個(gè)鉤子函數(shù)里請(qǐng)求數(shù)據(jù)而不是放到 mounted 的鉤子函數(shù)里葵袭?因?yàn)?requestData 是發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,這是一個(gè)異步過(guò)程乖菱,當(dāng)拿到響應(yīng)數(shù)據(jù)的時(shí)候坡锡,Vue 的 DOM 早就已經(jīng)渲染好了,但是數(shù)據(jù)改變 —> DOM 重新渲染仍然是一個(gè)異步過(guò)程窒所,所以即使在我們拿到數(shù)據(jù)后鹉勒,也要異步初始化 better-scroll。
數(shù)據(jù)的動(dòng)態(tài)更新
我們?cè)趯?shí)際開發(fā)中吵取,除了數(shù)據(jù)異步獲取禽额,還有一些場(chǎng)景可以動(dòng)態(tài)更新列表中的數(shù)據(jù),比如常見的下拉加載皮官,上拉刷新等脯倒。比如我們用 better-scroll 配合 Vue 實(shí)現(xiàn)下拉加載功能,代碼如下:
<template>
<div class="wrapper" ref="wrapper">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
<div class="loading-wrapper"></div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
data() {
return {data: []}
},
created() {
this.loadData()
},
methods: {
loadData() {
requestData().then((res) => {
this.data = res.data.concat(this.data)
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new Bscroll(this.$refs.wrapper, {})
this.scroll.on('touchend', (pos) => {
// 下拉動(dòng)作
if (pos.y > 50) {
this.loadData()
}
})
} else {
this.scroll.refresh()
}
})
})
}
}
}
</script>
這段代碼比之前稍微復(fù)雜一些, 當(dāng)我們?cè)诨瑒?dòng)列表松開手指時(shí)候捺氢, better-scroll 會(huì)對(duì)外派發(fā)一個(gè) touchend 事件藻丢,我們監(jiān)聽了這個(gè)事件,并且判斷了 pos.y > 50
(我們把這個(gè)行為定義成一次下拉的動(dòng)作)摄乒。如果是下拉的話我們會(huì)重新請(qǐng)求數(shù)據(jù)悠反,并且把新的數(shù)據(jù)和之前的 data 做一次 concat残黑,也就更新了列表的數(shù)據(jù),那么數(shù)據(jù)的改變就會(huì)映射到 DOM 的變化问慎。需要注意的一點(diǎn)萍摊,這里我們對(duì) this.scroll
做了判斷,如果沒(méi)有初始化過(guò)我們會(huì)通過(guò) new BScroll
初始化如叼,并且綁定一些事件冰木,否則我們會(huì)調(diào)用 this.scroll.refresh
方法重新計(jì)算,來(lái)確保滾動(dòng)效果的正常笼恰。
這里踊沸,我們就通過(guò) better-scroll 配合 Vue,實(shí)現(xiàn)了列表的下拉刷新功能社证,上拉加載也是類似的套路逼龟,一切看上去都是 ok 的。但是追葡,我們發(fā)現(xiàn)這里寫了大量命令式的代碼(這一點(diǎn)不是 Vue.js 推薦的)腺律,如果有很多類似滾動(dòng)的組件,我們就需要寫很多類似的命令式且重復(fù)性的代碼宜肉,而且我們把數(shù)據(jù)請(qǐng)求和 better-scroll 也做了強(qiáng)耦合匀钧,這些對(duì)于一個(gè)追求編程逼格的人來(lái)說(shuō),就不 ok 了谬返。
scroll 組件的抽象和封裝
因此之斯,我們有強(qiáng)烈的需求抽象出來(lái)一個(gè) scroll 組件,類似小程序的 scroll-view 組件遣铝,方便開發(fā)者的使用佑刷。
首先,我們要考慮的是 scroll 組件本質(zhì)上就是一個(gè)可以滾動(dòng)的列表組件酿炸,至于列表的 DOM 結(jié)構(gòu)瘫絮,只需要滿足 better-scroll 的 DOM 結(jié)構(gòu)規(guī)范即可,具體用什么標(biāo)簽填硕,有哪些輔助節(jié)點(diǎn)(比如下拉刷新上拉加載的 loading 層)檀何,這些都不是 scroll 組件需要關(guān)心的。因此廷支, scroll 組件的 DOM 結(jié)構(gòu)十分簡(jiǎn)單,如下所示:
<template>
<div ref="wrapper">
<slot></slot>
</div>
</template>
這里我們用到了 Vue 的特殊元素—— slot 插槽栓辜,它可以滿足我們靈活定制列表 DOM 結(jié)構(gòu)的需求恋拍。接下來(lái)我們來(lái)看看 JS 部分:
<script type="text/ecmascript-6">
import BScroll from 'better-scroll'
export default {
props: {
/*1 滾動(dòng)的時(shí)候會(huì)派發(fā)scroll事件,會(huì)截流藕甩。
2 滾動(dòng)的時(shí)候?qū)崟r(shí)派發(fā)scroll事件施敢,不會(huì)截流周荐。
3 除了實(shí)時(shí)派發(fā)scroll事件,在swipe的情況下仍然能實(shí)時(shí)派發(fā)scroll事件
*/
probeType: {
type: Number,
default: 1
},
// 點(diǎn)擊列表是否派發(fā)click事件
click: {
type: Boolean,
default: true
},
// 是否開啟橫向滾動(dòng)
scrollX: {
type: Boolean,
default: false
},
// 是否派發(fā)滾動(dòng)事件
listenScroll: {
type: Boolean,
default: false
},
// 列表的數(shù)據(jù)
data: {
type: Array,
default: null
},
/** * 是否派發(fā)滾動(dòng)到底部的事件僵娃,用于上拉加載 */
pullup: {
type: Boolean,
default: false
},
/** * 是否派發(fā)頂部下拉的事件概作,用于下拉刷新 */
pulldown: {
type: Boolean,
default: false
},
/** * 是否派發(fā)列表滾動(dòng)開始的事件 */
beforeScroll: {
type: Boolean,
default: false
},
/** * 當(dāng)數(shù)據(jù)更新后,刷新scroll的延時(shí)默怨。 */
refreshDelay: {
type: Number,
default: 20
}
},
mounted() {
// 保證在DOM渲染完畢后初始化better-scroll
setTimeout(() => {
this._initScroll()
}, 20)
},
methods: {
_initScroll() {
if (!this.$refs.wrapper) {
return
}
// better-scroll的初始化
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
click: this.click,
scrollX: this.scrollX
})
// 是否派發(fā)滾動(dòng)事件
if (this.listenScroll) {
let me = this
this.scroll.on('scroll', (pos) => {
me.$emit('scroll', pos)
})
}
// 是否派發(fā)滾動(dòng)到底部事件讯榕,用于上拉加載
if (this.pullup) {
this.scroll.on('scrollEnd', () => { // 滾動(dòng)到底部
if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
this.$emit('scrollToEnd')
}
})
}
// 是否派發(fā)頂部下拉事件,用于下拉刷新
if (this.pulldown) {
this.scroll.on('touchend', (pos) => { // 下拉動(dòng)作
if (pos.y > 50) {
this.$emit('pulldown')
}
})
}
// 是否派發(fā)列表滾動(dòng)開始的事件
if (this.beforeScroll) {
this.scroll.on('beforeScrollStart', () => {
this.$emit('beforeScroll')
})
}
},
disable() {
// 代理better-scroll的disable方法
this.scroll && this.scroll.disable()
},
enable() {
// 代理better-scroll的enable方法
this.scroll && this.scroll.enable()
},
refresh() {
// 代理better-scroll的refresh方法
this.scroll && this.scroll.refresh()
},
scrollTo() {
// 代理better-scroll的scrollTo方法
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
},
scrollToElement() {
// 代理better-scroll的scrollToElement方法
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
}
},
watch: {
// 監(jiān)聽數(shù)據(jù)的變化匙睹,延時(shí)refreshDelay時(shí)間后調(diào)用refresh方法重新計(jì)算愚屁,保證滾動(dòng)效果正常
data() {
setTimeout(() => {
this.refresh()
}, this.refreshDelay)
}
}
}
</script>
JS 部分實(shí)際上就是對(duì) better-scroll 做一層 Vue 的封裝,通過(guò) props 的形式痕檬,把一些對(duì) better-scroll 定制化的控制權(quán)交給父組件霎槐;通過(guò) methods 暴露的一些方法對(duì) better-scroll 的方法做一層代理;通過(guò) watch 傳入的 data梦谜,當(dāng) data 發(fā)生改變的時(shí)候丘跌,在適當(dāng)?shù)臅r(shí)機(jī)調(diào)用 refresh 方法重新計(jì)算 better-scroll 確保滾動(dòng)效果正常,這里之所以要有一個(gè) refreshDelay 的設(shè)置是考慮到如果我們對(duì)列表操作用到了 transition-group 做動(dòng)畫效果唁桩,那么 DOM 的渲染完畢時(shí)間就是在動(dòng)畫完成之后闭树。
有了這一層 scroll 組件的封裝,我們來(lái)修改剛剛最復(fù)雜的代碼(假設(shè)我們已經(jīng)全局注冊(cè)了 scroll 組件)朵夏。
<template>
<scroll class="wrapper" :data="data" :pulldown="pulldown" @pulldown="loadData">
<ul class="content">
<li v-for="item in data">{{item}}</li>
</ul>
<div class="loading-wrapper"></div>
</scroll>
</template>
<script>
import BScroll from 'better-scroll'
export default {
data() {
return {
data: [],
pulldown: true
}
},
created() {
this.loadData()
},
methods: {
loadData() {
requestData().then((res) => {
this.data = res.data.concat(this.data)
})
}
}
}
</script>
可以很明顯的看到我們的 JS 部分精簡(jiǎn)了非常多的代碼蔼啦,沒(méi)有對(duì) better-scroll 再做命令式的操作了,同時(shí)把數(shù)據(jù)請(qǐng)求和 better-scroll 也做了剝離仰猖,父組件只需要把數(shù)據(jù) data 通過(guò) prop 傳給 scroll 組件捏肢,就可以保證 scroll 組件的滾動(dòng)效果。同時(shí)饥侵,如果想實(shí)現(xiàn)下拉刷新的功能鸵赫,只需要通過(guò) prop 把 pulldown 設(shè)置為 true,并且監(jiān)聽 pulldown 的事件去做一些數(shù)據(jù)獲取并更新的動(dòng)作即可躏升,整個(gè)邏輯也是非常清晰的辩棒。