謝謝作者的文章? 非常喜歡? 請允許收藏隙笆!
vue之better-scroll的封裝,包含下拉刷新,上拉加載功能及UI(核心為借鑒升筏,我僅僅是給輪子套上了外胎...)
先發(fā)原文作者撑柔、地址等信息。我把內(nèi)容全部搬過來了您访,也可以去看原文铅忿。內(nèi)容絕對是滿滿的干貨,給原作者點贊A橥簟(我添加的內(nèi)容在轉(zhuǎn)載過來的后面檀训,內(nèi)容不多)
作者: ustbhuangyi
鏈接:http://www.imooc.com/article/18232來源:慕課網(wǎng)
在我們?nèi)粘5囊苿佣隧椖块_發(fā)中,處理滾動列表是再常見不過的需求了享言,以滴滴為例峻凫,可以是這樣豎向滾動的列表,如圖所示:
也可以是橫向滾動的導航欄览露,如圖所示:
可以打開“微信 —> 錢包—>滴滴出行”體驗效果荧琼。
我們在實現(xiàn)這類滾動功能的時候,會用到我寫的第三方庫差牛,better-scroll命锄。
什么是 better-scroll
better-scroll 是一個移動端滾動的解決方案,它是基于 iscroll 的重寫多糠,它和 iscroll 的主要區(qū)別在這里累舷。better-scroll 也很強大,不僅可以做普通的滾動列表夹孔,還可以做輪播圖被盈、picker 等等析孽。
better-scroll 的滾動原理
不少同學可能用過 better-scroll,我收到反饋最多的問題是:
我的 better-scroll 初始化了只怎, 但是沒法滾動袜瞬。
不能滾動是現(xiàn)象,我們得搞清楚這其中的根本原因身堡。在這之前邓尤,我們先來看一下瀏覽器的滾動原理:
瀏覽器的滾動條大家都會遇到,當頁面內(nèi)容的高度超過視口高度的時候贴谎,會出現(xiàn)縱向滾動條汞扎;當頁面內(nèi)容的寬度超過視口寬度的時候,會出現(xiàn)橫向滾動條擅这。也就是當我們的視口展示不下內(nèi)容的時候澈魄,會通過滾動條的方式讓用戶滾動屏幕看到剩余的內(nèi)容。
那么對于 better-scroll 也是一樣的道理仲翎,我們先來看一下 better-scroll 常見的 html 結(jié)構(gòu):
為了更加直觀痹扇,我們再來看一張圖:
綠色部分為 wrapper,也就是父容器溯香,它會有固定的高度鲫构。黃色部分為 content,它是父容器的第一個子元素玫坛,它的高度會隨著內(nèi)容的大小而撐高结笨。那么,當 content 的高度不超過父容器的高度湿镀,是不能滾動的禀梳,而它一旦超過了父容器的高度,我們就可以滾動內(nèi)容區(qū)了肠骆,這就是 better-scroll 的滾動原理算途。
那么,我們怎么初始化 better-scroll 呢蚀腿,如果是上述 html 結(jié)構(gòu)嘴瓤,那么初始化代碼如下:
import BScroll from 'better-scroll'let wrapper= document.querySelector('.wrapper')
let scroll=newBScroll(wrapper, {})
better-scroll 對外暴露了一個 BScroll 的類,我們初始化只需要 new 一個類的實例即可莉钙。第一個參數(shù)就是我們 wrapper 的 DOM 對象廓脆,第二個是一些配置參數(shù),具體參考better-scroll 的文檔磁玉。
better-scroll 的初始化時機很重要停忿,因為它在初始化的時候,會計算父元素和子元素的高度和寬度蚊伞,來決定是否可以縱向和橫向滾動席赂。因此吮铭,我們在初始化它的時候,必須確保父元素和子元素的內(nèi)容已經(jīng)正確渲染了颅停。如果子元素或者父元素 DOM 結(jié)構(gòu)發(fā)生改變的時候谓晌,必須重新調(diào)用scroll.refresh()方法重新計算來確保滾動效果的正常。所以同學們反饋的 better-scroll 不能滾動的原因多半是初始化 better-scroll 的時機不對癞揉,或者是當 DOM 結(jié)構(gòu)發(fā)送變化的時候并沒有重新計算 better-scroll纸肉。
better-scroll 遇見 Vue
相信很多同學對Vue.js都不陌生,當 better-scroll 遇見 Vue喊熟,會擦出怎樣的火花呢柏肪?
如何在 Vue 中使用 better-scroll
很多同學開始接觸使用 better-scroll 都是受到了我的一門教學課程——《Vue.js高仿餓了么外賣App》的影響。在那門課程中芥牌,我們把 better-scroll 和 Vue 做了結(jié)合预吆,實現(xiàn)了很多列表滾動的效果。在 Vue 中的使用方法如下:
?
- ? ? ?
- ... ? ? ?
- ... ...
mounted() {this.$nextTick(() =>{this.scroll =newBscroll(this.$refs.wrapper, {})
})
}
}
Vue.js 提供了我們一個獲取 DOM 對象的接口——vm.$refs胳泉。在這里,我們通過了this.$refs.wrapper訪問到了這個 DOM 對象岩遗,并且我們在 mounted 這個鉤子函數(shù)里扇商,this.$nextTick的回調(diào)函數(shù)中初始化 better-scroll 。因為這個時候宿礁,wrapper 的 DOM 已經(jīng)渲染了案铺,我們可以正確計算它以及它內(nèi)層 content 的高度,以確保滾動正常梆靖。
這里的this.$nextTick是一個異步函數(shù)控汉,為了確保 DOM 已經(jīng)渲染,感興趣的同學可以了解一下它的內(nèi)部實現(xiàn)細節(jié)返吻,底層用到了 MutationObserver 或者是setTimeout(fn, 0)姑子。其實我們在這里把this.$nextTick替換成setTimeout(fn, 20)也是可以的(20 ms 是一個經(jīng)驗值,每一個 Tick 約為 17 ms)测僵,對用戶體驗而言都是無感知的街佑。
異步數(shù)據(jù)的處理
在我們的實際工作中,列表的數(shù)據(jù)往往都是異步獲取的捍靠,因此我們初始化 better-scroll 的時機需要在數(shù)據(jù)獲取后沐旨,代碼如下:
?
- ? ? ?
- {{item}} ? ?
data() {return{
data: []
}
},
created() {
requestData().then((res)=>{this.data =res.datathis.$nextTick(() =>{this.scroll =newBscroll(this.$refs.wrapper, {})
})
})
}
}
這里的 requestData 是偽代碼,作用就是發(fā)起一個 http 請求從服務端獲取數(shù)據(jù)榨婆,并且這個函數(shù)返回的是一個 promise(實際項目中我們可能會用axios或者vue-resource)磁携。我們獲取到數(shù)據(jù)的后,需要通過異步的方式再去初始化 better-scroll良风,因為 Vue 是數(shù)據(jù)驅(qū)動的谊迄, Vue 數(shù)據(jù)發(fā)生變化(this.data = res.data)到頁面重新渲染是一個異步的過程闷供,我們的初始化時機是要在 DOM 重新渲染后,所以這里用到了this.$nextTick鳞上,當然替換成setTimeout(fn, 20)也是可以的这吻。
為什么這里在 created 這個鉤子函數(shù)里請求數(shù)據(jù)而不是放到 mounted 的鉤子函數(shù)里?因為 requestData 是發(fā)送一個網(wǎng)絡請求篙议,這是一個異步過程唾糯,當拿到響應數(shù)據(jù)的時候,Vue 的 DOM 早就已經(jīng)渲染好了鬼贱,但是數(shù)據(jù)改變 —> DOM 重新渲染仍然是一個異步過程移怯,所以即使在我們拿到數(shù)據(jù)后,也要異步初始化 better-scroll这难。
數(shù)據(jù)的動態(tài)更新
我們在實際開發(fā)中舟误,除了數(shù)據(jù)異步獲取,還有一些場景可以動態(tài)更新列表中的數(shù)據(jù)姻乓,比如常見的下拉加載嵌溢,上拉刷新等。比如我們用 better-scroll 配合 Vue 實現(xiàn)下拉加載功能蹋岩,代碼如下:
?
- ? ? ?
- {{item}} ? ?
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 =newBscroll(this.$refs.wrapper, {})this.scroll.on('touchend', (pos) =>{//下拉動作if(pos.y > 50) {this.loadData()
}
})
}else{this.scroll.refresh()
}
})
})
}
}
}
這段代碼比之前稍微復雜一些, 當我們在滑動列表松開手指時候赖草, better-scroll 會對外派發(fā)一個 touchend 事件,我們監(jiān)聽了這個事件剪个,并且判斷了 pos.y > 50(我們把這個行為定義成一次下拉的動作)秧骑。如果是下拉的話我們會重新請求數(shù)據(jù),并且把新的數(shù)據(jù)和之前的 data 做一次 concat扣囊,也就更新了列表的數(shù)據(jù)乎折,那么數(shù)據(jù)的改變就會映射到 DOM 的變化。需要注意的一點侵歇,這里我們對this.scroll做了判斷骂澄,如果沒有初始化過我們會通過new BScroll初始化,并且綁定一些事件惕虑,否則我們會調(diào)用this.scroll.refresh方法重新計算酗洒,來確保滾動效果的正常。
這里枷遂,我們就通過 better-scroll 配合 Vue樱衷,實現(xiàn)了列表的下拉刷新功能,上拉加載也是類似的套路酒唉,一切看上去都是 ok 的矩桂。但是,我們發(fā)現(xiàn)這里寫了大量命令式的代碼(這一點不是 Vue.js 推薦的),如果有很多類似滾動的組件侄榴,我們就需要寫很多類似的命令式且重復性的代碼雹锣,而且我們把數(shù)據(jù)請求和 better-scroll 也做了強耦合,這些對于一個追求編程逼格的人來說癞蚕,就不 ok 了蕊爵。
scroll 組件的抽象和封裝
因此,我們有強烈的需求抽象出來一個 scroll 組件桦山,類似小程序的 scroll-view 組件攒射,方便開發(fā)者的使用。
首先恒水,我們要考慮的是 scroll 組件本質(zhì)上就是一個可以滾動的列表組件会放,至于列表的 DOM 結(jié)構(gòu),只需要滿足 better-scroll 的 DOM 結(jié)構(gòu)規(guī)范即可钉凌,具體用什么標簽咧最,有哪些輔助節(jié)點(比如下拉刷新上拉加載的 loading 層),這些都不是 scroll 組件需要關心的御雕。因此矢沿, scroll 組件的 DOM 結(jié)構(gòu)十分簡單,如下所示:
這里我們用到了 Vue 的特殊元素—— slot 插槽酸纲,它可以滿足我們靈活定制列表 DOM 結(jié)構(gòu)的需求捣鲸。接下來我們來看看 JS 部分:
import BScroll from'better-scroll'exportdefault{
props: {/**
* 1 滾動的時候會派發(fā)scroll事件,會截流福青。
* 2 滾動的時候?qū)崟r派發(fā)scroll事件,不會截流脓诡。
* 3 除了實時派發(fā)scroll事件无午,在swipe的情況下仍然能實時派發(fā)scroll事件*/probeType: {
type: Number,default: 1},/**
* 點擊列表是否派發(fā)click事件*/click: {
type: Boolean,default:true},/**
* 是否開啟橫向滾動*/scrollX: {
type: Boolean,default:false},/**
* 是否派發(fā)滾動事件*/listenScroll: {
type: Boolean,default:false},/**
* 列表的數(shù)據(jù)*/data: {
type: Array,default:null},/**
* 是否派發(fā)滾動到底部的事件,用于上拉加載*/pullup: {
type: Boolean,default:false},/**
* 是否派發(fā)頂部下拉的事件祝谚,用于下拉刷新*/pulldown: {
type: Boolean,default:false},/**
* 是否派發(fā)列表滾動開始的事件*/beforeScroll: {
type: Boolean,default:false},/**
* 當數(shù)據(jù)更新后宪迟,刷新scroll的延時。*/refreshDelay: {
type: Number,default: 20}
},
mounted() {//保證在DOM渲染完畢后初始化better-scrollsetTimeout(() =>{this._initScroll()
},20)
},
methods: {
_initScroll() {if(!this.$refs.wrapper) {return}//better-scroll的初始化this.scroll =newBScroll(this.$refs.wrapper, {
probeType:this.probeType,
click:this.click,
scrollX:this.scrollX
})//是否派發(fā)滾動事件if(this.listenScroll) {
let me=thisthis.scroll.on('scroll', (pos) =>{
me.$emit('scroll', pos)
})
}//是否派發(fā)滾動到底部事件交惯,用于上拉加載if(this.pullup) {this.scroll.on('scrollEnd', () =>{//滾動到底部if(this.scroll.y <= (this.scroll.maxScrollY + 50)) {this.$emit('scrollToEnd')
}
})
}//是否派發(fā)頂部下拉事件次泽,用于下拉刷新if(this.pulldown) {this.scroll.on('touchend', (pos) =>{//下拉動作if(pos.y > 50) {this.$emit('pulldown')
}
})
}//是否派發(fā)列表滾動開始的事件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ù)的變化,延時refreshDelay時間后調(diào)用refresh方法重新計算席爽,保證滾動效果正常data(){
setTimeout(()=>{this.refresh()},this.refreshDelay)}}}
JS 部分實際上就是對 better-scroll 做一層 Vue 的封裝意荤,通過 props 的形式,把一些對 better-scroll 定制化的控制權(quán)交給父組件只锻;通過 methods 暴露的一些方法對 better-scroll 的方法做一層代理玖像;通過 watch 傳入的 data,當 data 發(fā)生改變的時候齐饮,在適當?shù)臅r機調(diào)用 refresh 方法重新計算 better-scroll 確保滾動效果正常捐寥,這里之所以要有一個 refreshDelay 的設置是考慮到如果我們對列表操作用到了 transition-group 做動畫效果笤昨,那么 DOM 的渲染完畢時間就是在動畫完成之后。
有了這一層 scroll 組件的封裝握恳,我們來修改剛剛最復雜的代碼(假設我們已經(jīng)全局注冊了 scroll 組件)瞒窒。
? ? ?
- ? ? ?
- {{item}} ? ?
data() {return{
data: [],
pulldown:true}
},
created() {this.loadData()
},
methods: {
loadData() {
requestData().then((res)=>{this.data = res.data.concat(this.data)
})
}
}
}
可以很明顯的看到我們的 JS 部分精簡了非常多的代碼,沒有對 better-scroll 再做命令式的操作了乡洼,同時把數(shù)據(jù)請求和 better-scroll 也做了剝離崇裁,父組件只需要把數(shù)據(jù) data 通過 prop 傳給 scroll 組件,就可以保證 scroll 組件的滾動效果就珠。同時寇壳,如果想實現(xiàn)下拉刷新的功能,只需要通過 prop 把 pulldown 設置為 true妻怎,并且監(jiān)聽 pulldown 的事件去做一些數(shù)據(jù)獲取并更新的動作即可壳炎,整個邏輯也是非常清晰的。
插件 Vue 化引發(fā)的一些思考
這篇文章我不僅僅是要教會大家封裝一個 scroll 組件逼侦,還想傳遞一些把第三方插件(原生 JS 實現(xiàn))Vue 化的思考過程匿辩。很多學習 Vue.js 的同學可能還停留在 “XX 效果如何用 Vue.js 實現(xiàn)” 的程度,其實把插件 Vue 化有兩點很關鍵榛丢,一個是對插件本身的實現(xiàn)原理很了解铲球,另一個是對 Vue.js 的特性很了解。對插件本身的實現(xiàn)原理了解需要的是一個思考和鉆研的過程晰赞,這個過程可能困難稼病,但是收獲也是巨大的;而對 Vue.js 的特性的了解掖鱼,是需要大家對 Vue.js 多多使用然走,學會從平時的項目中積累和總結(jié),也要善于查閱 Vue.js 的官方文檔戏挡,關注一些 Vue.js 的升級等芍瑞。
所以,我們拒絕伸手黨褐墅,但也不是鼓勵大家什么時候都要去造輪子拆檬,當我們在使用一些現(xiàn)成插件的同時,也希望大家能多多思考妥凳,去探索一下現(xiàn)象背后的本質(zhì)竟贯,把 “XX 效果如何用 Vue.js 實現(xiàn)” 這句話從問號變成句號。
以下內(nèi)容是我在作者基礎上添加了一些交互效果逝钥,和作者的放在一起做成一個組件澄耍,可以直接拿去用。為了更容易看懂我的思路,進行了簡要的注釋齐莲。
props: {/**
* 1 滾動的時候會派發(fā)scroll事件痢站,會截流。
* 2 滾動的時候?qū)崟r派發(fā)scroll事件选酗,不會截流阵难。
* 3 除了實時派發(fā)scroll事件,在swipe的情況下仍然能實時派發(fā)scroll事件*/probeType: {
type: Number,default:1},/**
* 點擊列表是否派發(fā)click事件*/click: {
type: Boolean,default:true},/**
* 是否開啟橫向滾動*/scrollX: {
type: Boolean,default:false},/**
* 是否派發(fā)滾動事件*/listenScroll: {
type: Boolean,default:false},/**
* 列表的數(shù)據(jù)*/data: {
type: Array,default:null},/**
* 是否派發(fā)滾動到底部的事件芒填,用于上拉加載*/pullup: {
type: Boolean,default:false},/**
* 是否派發(fā)頂部下拉的事件呜叫,用于下拉刷新*/pulldown: {
type: Boolean,default:false},/**
* 是否派發(fā)列表滾動開始的事件*/beforeScroll: {
type: Boolean,default:false},/**
* 當數(shù)據(jù)更新后,刷新scroll的延時殿衰。*/refreshDelay: {
type: Number,default:20},/**
* 如果啟用loading交互朱庆,傳遞loading的狀態(tài)
* isShow: false
* showIcon: false,? ? // 是否顯示loading的icon
* status: ''? // '正在加載...', '刷新成功', '刷新失敗', ''*/loadingStatus: {
type: Object,default:function() {return{
showIcon:false,
status:''};
}
},/**
* 是否啟用下拉刷新的交互*/pulldownUI: {
type: Boolean,default:false},/**
* 是否啟用上拉加載的交互*/pullupUI: {
type: Boolean,default:false}
},
data() {return{
loadingConnecting:false,
pulldownTip: {
text:'下拉刷新',//松開立即刷新rotate:''//icon-rotate},
};
},
mounted() {//保證在DOM渲染完畢后初始化better-scrollsetTimeout(()=>{this._initScroll()
},20)
},
methods: {
_initScroll() {if(!this.$refs.wrapper) {return;
}//better-scroll的初始化this.scroll=newBScroll(this.$refs.wrapper, {
probeType:this.probeType,
click:this.click,
scrollX:this.scrollX
});//是否派發(fā)滾動事件if(this.listenScroll||this.pulldown||this.pullup) {
let me=this;this.scroll.on('scroll', (pos)=>{if(this.listenScroll) {
me.$emit('scroll', pos);
}if(this.pulldown) {//下拉動作if(pos.y>50) {this.pulldownTip={
text:'松開立即刷新',
rotate:'icon-rotate'}
}else{this.pulldownTip={
text:'下拉刷新',//松開立即刷新rotate:''//icon-rotate}
}
}if(this.pullup) {
}
})
}//是否派發(fā)滾動到底部事件,用于上拉加載if(this.pullup) {this.scroll.on('scrollEnd', ()=>{
console.log('scrollEnd');
console.log(this.scroll);//滾動到底部if(this.scroll.y<=(this.scroll.maxScrollY+50)) {this.$emit('scrollToEnd');
}
});
}//是否派發(fā)頂部下拉事件闷祥,用于下拉刷新if(this.pulldown) {this.scroll.on('touchend', (pos)=>{//下拉動作if(pos.y>50) {
setTimeout(()=>{//重置提示信息this.pulldownTip={
text:'下拉刷新',//松開立即刷新rotate:''//icon-rotate}
},600);this.$emit('pulldown');
}
});
}//是否派發(fā)列表滾動開始的事件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ù)的變化娱颊,延時refreshDelay時間后調(diào)用refresh方法重新計算,保證滾動效果正常data() {
setTimeout(()=>{this.refresh();
},this.refreshDelay);
}
}
}$cube-size: 10px; // 項目中用了scss凯砍,沒用的話箱硕,替換掉樣式中的變量即可
.better-scroll-root{background-color:rgba(7, 17, 27, 0.7);.loading-pos, .pulldown-tip {
position:absolute;left:0;top:0;width:100%;height:35px;color:#fcfcfc;text-align:center;z-index:2000;}.loading-pos{background-color:rgba(7, 17, 27, 0.7);}.pulldown-tip{top:-50px;height:50px;line-height:50px;z-index:1;}.pull-icon{position:absolute;top:0;left:30%;color:#a5a1a1;font-size:1.5em;transition:all 0.15s ease-in-out;}.pull-icon.icon-rotate{transform:rotate(180deg);}.loading-container{position:absolute;height:$cube-size;width:$cube-size;left:35%;top:50%;transform:translate(-50%, -50%);perspective:40px;}.loading-connecting{line-height:35px;}.cube{height:$cube-size;width:$cube-size;transform-origin:50% 50%;transform-style:preserve-3d;animation:rotate 3s infinite ease-in-out;}.side{position:absolute;height:$cube-size;width:$cube-size;border-radius:50%;}.side1{background:#4bc393;transform:translateZ($cube-size);}.side2{background:#FF884D;transform:rotateY(90deg) translateZ($cube-size);}.side3{background:#32526E;transform:rotateY(180deg) translateZ($cube-size);}.side4{background:#c53fa3;transform:rotateY(-90deg) translateZ($cube-size);}.side5{background:#FFCC5C;transform:rotateX(90deg) translateZ($cube-size);}.side6{background:#FF6B57;transform:rotateX(-90deg) translateZ($cube-size);}@keyframes rotate{0%{
transform:rotateX(0deg) rotateY(0deg);}50%{transform:rotateX(360deg) rotateY(0deg);}100%{transform:rotateX(360deg) rotateY(360deg);}}
}
下拉刷新,上拉加載(暫時未做)悟衩,刷新中等效果如下:
need-to-insert-img
need-to-insert-img
need-to-insert-img
以上內(nèi)容還不夠精細剧罩,等這段時間忙過去了會繼續(xù)優(yōu)化。如有bug座泳,歡迎各位看官批評指正惠昔。