前言
一個(gè)前端小開(kāi)發(fā)仔,封裝一個(gè)適銷對(duì)路的組件,是我們必備的基本功。
本文以實(shí)現(xiàn)一個(gè)vue的滾動(dòng)監(jiān)聽(tīng)組件,總結(jié)一下如何封裝一個(gè)造福后人(能用)的公共組件。

需求場(chǎng)景
H5應(yīng)用中,為了減輕后臺(tái)查詢數(shù)據(jù)的壓力,列表常以滾動(dòng)加載的形式實(shí)現(xiàn)。
即首次進(jìn)入頁(yè)面時(shí),只請(qǐng)求若干條數(shù)據(jù)。當(dāng)頁(yè)面滾動(dòng)到底部時(shí),再次請(qǐng)求接口返回?cái)?shù)據(jù)。
需求分析
由于這種業(yè)務(wù)場(chǎng)景十分常見(jiàn),因此可封裝一個(gè)通用的滾動(dòng)監(jiān)聽(tīng)組件,監(jiān)聽(tīng)頁(yè)面滾動(dòng)的位置。當(dāng)觸發(fā)特定條件時(shí),向外派發(fā)一個(gè)事件。
功能實(shí)現(xiàn)
頁(yè)面滾動(dòng)監(jiān)聽(tīng)
- 當(dāng)父元素的固定高度 < 內(nèi)部子元素的總高度,且父元素 overflow: scroll 時(shí),可以監(jiān)聽(tīng)到父元素的 scroll 事件。因此,滾動(dòng)列表需要插在scroll里。這里我們需要用到一個(gè) slot 來(lái)實(shí)現(xiàn)。
- 由于需要綁定DOM事件,需要獲得對(duì)DOM的控制。這里我們需要用到一個(gè) refs 來(lái)實(shí)現(xiàn)。
- 當(dāng)觸發(fā)監(jiān)聽(tīng)的條件時(shí),我們需要對(duì)調(diào)用scroll組件的組件派發(fā)一個(gè)事件,以通知進(jìn)行下一步的邏輯。這里我們需要用到一個(gè) emit 來(lái)實(shí)現(xiàn)。
至此,一個(gè)滾動(dòng)組件的主要功能就出來(lái)了。但是,作為一個(gè)有追求的組件,我們是不是還可以改進(jìn)一下呢。
優(yōu)化
防抖
滾動(dòng)事件觸發(fā)頻率很高,因此可以做一個(gè)防抖操作,只在最后一次滾動(dòng)事件觸發(fā)時(shí)做判斷滾動(dòng)位置的操作。
綁定移除
當(dāng)后臺(tái)無(wú)相關(guān)數(shù)據(jù)時(shí),就不需要再監(jiān)聽(tīng)滾動(dòng)事件,寫(xiě)一個(gè)解除綁定的方法,以供按需調(diào)用。
解決慣性滾動(dòng)問(wèn)題
手機(jī)實(shí)測(cè)時(shí),滾動(dòng)事件只在手指滑動(dòng)接觸屏幕時(shí)觸發(fā)滾動(dòng)事件。因此,當(dāng)判定未到達(dá)底部時(shí),寫(xiě)一個(gè)延時(shí)判斷邏輯,判斷是否在慣性滾動(dòng)其間,元素滾動(dòng)到底部。(各位大佬有其他解決辦法,可以評(píng)論提一下)
注:記得添加 -webkit-overflow-scrolling: touch; 否則IOS端不實(shí)現(xiàn)慣性滾動(dòng)
增加通用性
當(dāng)前設(shè)計(jì)下,觸發(fā)派發(fā)事件的滾動(dòng)位置、防抖的間隔、refs的值都是固定的。
但對(duì)于不同列表,可能需要提早做一些預(yù)加載數(shù)據(jù)、操作一些滾動(dòng)組件的DOM等。
因此這些常用的參數(shù),應(yīng)該通過(guò) props 對(duì)外暴露出來(lái),讓父組件傳入。
如果定義了一個(gè)是Object類型的props,可能會(huì)出現(xiàn)某些字段不傳的情況。
這時(shí),組件里利用 三元表達(dá)式 ,Object.key ? Object.key : defaultValue ,可使得組件有良好的默認(rèn)表現(xiàn)。
效果如下:

demo地址:https://github.com/yejiayuan163/simpleScroll
滾動(dòng)組件代碼見(jiàn)文末
總結(jié)
寫(xiě)完一個(gè)滾動(dòng)組件的開(kāi)發(fā)流程,現(xiàn)在可以總結(jié)一下開(kāi)發(fā)一個(gè)vue公共組件的原則
- 只關(guān)注公共組件自身的功能。 公共組件只關(guān)注自己的狀態(tài),不關(guān)心頁(yè)面數(shù)據(jù)邏輯。
- 設(shè)計(jì)好與父組件的通信。 父組件通過(guò)props控制公共組件的表現(xiàn)。通過(guò)emit的事件,監(jiān)聽(tīng)到公共組件狀態(tài)。通過(guò)refs調(diào)用到公共組件的方法。當(dāng)然用vuex+computed屬性監(jiān)聽(tīng)也是可以的。
- 有良好的默認(rèn)表現(xiàn) 使用者既可定制組件的表現(xiàn),也可以簡(jiǎn)單地使用默認(rèn)配置。
- 記得寫(xiě)使用文檔。
謝謝觀看!
附?代碼實(shí)現(xiàn)
<!--調(diào)用時(shí),flbScroll的父元素需要固定高度,flbScroll包裹的元素(需要被監(jiān)聽(tīng)的元素),高度不可固定,需要由內(nèi)部元素?fù)伍_(kāi)-->
<!--移動(dòng)端使用,故未對(duì)ie8做兼容處理-->
<template>
<div class="scrollWrapper" :ref="wrapperName">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'y-scroll',
props:{
// 被監(jiān)聽(tīng)元素距離底部的距離
disFromBottom:{
type: Number,
default: 300
},
// 被監(jiān)聽(tīng)元素距離頂部的距離
disFromTop:{
type: Number,
default: 300
},
// 給scroll組件外層定義一個(gè)ref,用于獲取監(jiān)聽(tīng)滾動(dòng)
wrapperName:{
type: String,
default: '1'
},
//觸發(fā)判斷的時(shí)間間隔
interval: {
type: Number,
default: 300
},
},
data() {
return {
lastTime: 0,
timer:''
}
},
mounted() {
// 監(jiān)聽(tīng)滾動(dòng)事件
this.$nextTick(()=> {
this.$refs[this.wrapperName].addEventListener('scroll', this.listenPosition, true);
})
},
methods:{
listenPosition(){
if(this.lastTime + this.interval < new Date().getTime()){
this.lastTime = new Date().getTime()
const scrollTop = this.$refs[this.wrapperName].scrollTop//卷去的高度
const scrollHeight = this.$refs[this.wrapperName].scrollHeight//正文全文高
const height = this.$refs[this.wrapperName].clientHeight//元素的高度
// console.log('scrollTop:',scrollTop,'scrollHeight:',scrollHeight,'height:',height)
if(scrollHeight <= scrollTop + height + this.disFromBottom){
console.log('到底了?。?)
if(this.timer){
clearTimeout(this.timer)
}
this.$emit('onBottom')
} else {
// 解決慣性滾動(dòng)到底部不觸發(fā)的問(wèn)題
this.timer=setTimeout(() => {
const scrollTop = this.$refs[this.wrapperName].scrollTop//卷去的高度
const scrollHeight = this.$refs[this.wrapperName].scrollHeight//正文全文高
const height = this.$refs[this.wrapperName].clientHeight//元素的高度
if(scrollHeight <= scrollTop + height + this.disFromBottom){
this.$emit('onBottom')
}
},1000)
}
}
},
//解除滾動(dòng)綁定
unbindScroll(){
this.$refs[this.wrapperName].removeEventListener('scroll', this.listenPosition, true);//第三個(gè)參數(shù)布爾值與綁定時(shí)要一致,第二個(gè)參數(shù)不能用匿名函數(shù)
console.log('解綁滾動(dòng)事件')
}
},
beforeDestroy(){
// 移除監(jiān)聽(tīng)滾動(dòng)事件
this.$refs[this.wrapperName].removeEventListener('scroll', this.listenPosition, true);
}
}
</script>
<style scoped>
.scrollWrapper{
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
</style>