圖片輪播是種很常見的場景和功能,通常移動網站首頁的輪播 banner
,商品閒情頁的商品圖片等位置都會用到此功能css
像這種經常使用的場景功能確定是有人早就寫好插件了的,因此遇到這種場景,通常都遵循如下三步:html
swiper
、slider
、Album
等關鍵字npm install
之這種作法沒毛病,有現成的輪子可用固然拿來主義,由於項目用的是 vue
,因此我在網上找了一圈 基於 vue
的輪播組件庫,找到了兩個比較滿意的庫:vue-awesome-swiper、vue-swipe前端
比較知名的輪播框架,通常都會優先使用這個庫,功能豐富,適用於各類輪播場景,什麼 左右按鈕,動態指示點、進度條指示器、垂直切換、一次性顯示多個 slides
……功能簡直不要太完善 but 我只是想用其中一小部分基本功能而已,如此多的功能於我而言不只是看文檔費勁,更關鍵的是會在項目中引入太多的冗餘代碼,好不容易經過各類手段將代碼體積降下來,結果就由於引入了一個包一下回到解放前,要不得要不得vue
餓了麼前端團隊出品的一個庫,比較精簡,代碼量也不多,但又過於精簡了,例如不支持無限輪播,不支持自定義 swiperItem
,並且總感受有些生硬的感受git
至於其餘本人可以搜索到的庫,都沒什麼名氣或者下載量過小,不敢輕易在生產環境引入,因而就萌生了本身造個輪子來搞定這件事,這樣組價庫的功能和代碼體積本身都能控制,就算有什麼 bug
也能很快自行修正github
先看下最終實現效果:npm
或者你想本身體驗一下,這裏也有個寫好的 Demo數組
我已經將此功能打包成了一個
npm package
,可直接下載安裝使用,包括樣式在內的代碼體積壓縮後不到18KB
,Gzipped以後不到7KB
,源碼 已上傳瀏覽器
爲了描述方便,先定義一下名詞,將每個滑動小塊稱爲 swiperItem
,將容納全部滑動小塊的容器稱爲 swiper
:app
目前大多數的滑動組件庫,都是經過兩種方式實現組件的滑動的
第一種,同一時間只渲染三個 swiperItem
,每次滑動到下一個 swiperItem
以後,當即更新這三個 swiperItem
這種作法的優勢是,不管有多少個 swiperItem
都不會影響到瀏覽器的渲染性能,由於不管多少個,每次都只渲染其中的三個,缺點在於若是 swiperItem
的數量原本就少於三個,就須要額外的處理了,並且由於每次最多隻能滑動一個 swiperItem
的距離,使用起來不是那麼順滑,vue-swipe採用的是這種
第二種,一次性渲染全部的 swiperItem
,而且有時候爲了更順滑的體驗,還會在原 swiperItem
的首尾,再各添加一個 swiperItem
例如,原 swiperItem
的數據爲 1, 2, 3, 4, 5
,處理以後變成 5, 1, 2, 3, 4, 5, 1
,vue-awesome-swiper採用的是這種
優勢在於使用起來更順滑,缺點是若是數據量不少,好比有幾百幾千個的數據量,會影響到瀏覽器的渲染性能,但通常狀況下也不會有那麼大的數據量,幾十個都已經不多了
綜合考慮之下,本人決定採用第二種
本組件庫提供了兩種傳入 swiperItem
數據的方式
props
傳入一個圖片的數組通常來講,輪播組件主要元素都只是一張展現用的圖片,因此直接經過 props
傳入圖片數組的方式基本上能夠知足大部分需求
<swiper :urlList="urlList" />
複製代碼
對於這種狀況下的首尾追加操做就比較簡單,其實就是操做一個數組:
this.currentList = this.urlList.length > 1
? this.urlList.slice(-1).concat(this.urlList, this.urlList.slice(0, 1)).map((url, index) => ({ url, _id: index }))
: this.urlList.map((url, index) => ({ url, _id: index }))
複製代碼
而後直接渲染到模板上便可:
<div class="img-box" v-for="item in currentList" :key="item._id" :style="{ backgroundImage: `url(${item.url})`, backgroundSize }"></div>
複製代碼
順便說下關於圖片佈局的問題,我沒有直接寫個 img
元素而是將圖片當成了背景圖渲染,這種處理的好處在於,能夠很輕鬆地實現對圖片不管是長寬大小仍是位置的 UI
控制,想要圖片徹底顯示那就 background-size: contain
,想要徹底充滿那就 background-size: cover
,或者直接具體到像素的調整,水平垂直居中也根本不用什麼 display: flex;
,這東西在某些狀況的某些設備上很容易出現兼容問題,直接 background-position: 50%;
搞定
延伸開來,平時作需求碰到一些小 icon
的佈局,也徹底能夠採用這種方式,對齊起來很是順手,根本不用拿什麼 vertical-align
慢慢調,也不會有任何兼容問題
swiperItem
子組件這種方式給了開發者很高的定製化空間,可以自定義 swiperItem
的內容而不只限於一張圖片,但作起啦稍微有點麻煩,由於 slot
做爲組件層面的東西,不太好動態處理,難不成直接操縱原生API
?能夠是能夠,但既然都已經用框架了,再直接改 DOM
彷佛氣氛有點不太對……糾結許久,後來想到了動態組件 component
以及 render
函數,這才解決
主要思路就是傳入 swiperItem
當成 slot
正常渲染在 swiper
這個父組件內,但與此同時,在slot
的先後,再各渲染一個 component
動態組件:
<swiper>
<swiperItem />
<swiperItem />
<swiperItem />
</swiper>
複製代碼
<!-- 這是 swiper父組件 -->
<component :is="firstSwiperItem"></component>
<slot></slot>
<component :is="lastSwiperItem"></component>
複製代碼
這兩個放在 slot
先後位置的 component
動態組件 firstSwiperItem
和 lastSwiperItem
,就是上面說的 5,1,2,3,4,5,1
中的 5
和 1
:
updateChild (slots) {
this.firstSwiperItem = {
render (h) {
return h('div', {
staticClass: 'swiper-item-box'
}, slots.slice(-1))
}
}
this.lastSwiperItem = {
render (h) {
return h('div', {
staticClass: 'swiper-item-box'
}, slots.slice(0, 1))
}
}
}
複製代碼
其實一開始我是想經過 template
來解決這件事的,更簡單一點,但由於要使用 template
就必須引用同時包含運行時和編譯器的完整版本的 vue
,性價比過低,也不適合生產環境,因此最終仍是選擇了 render
函數
對 touch
事件的監聽,結合 translate3d
實時改變位移,就是滑動的精髓所在
在
touchstart
事件中記錄起始位置座標,在touchmove
事件中計算距離差進行實時位置的改變,在touchend
中進行收尾
邏輯上是很清晰的,但一些細節方面的東西處理起來仍是有點頭疼的
例如,若是用戶用多隻手指操做的怎麼辦?若是 touchstart
的時候用是兩指,touchmove
的時候就剩下單指怎麼辦?若是用戶先左滑右滑,怎麼判斷相比於初始究竟是左滑仍是右滑?若是連續滑過多個 swiperItem
,怎麼判斷結束時究竟是左滑仍是右滑……
若是用戶老老實實按照 最佳操做指南 來使用,這些問題固然不存在,可是你不可能要求用戶這麼作的,因此就必須解決這些問題
對於多指操做的問題,我一概以 e.touches
列表中最後一個爲準:
stStartX = e.touches[touchCount - 1].clientX
複製代碼
左滑右滑的問題,則經過 diffX
與基準值 criticalWidth
的比較,結合滑動座標 toX
進行雙重判斷,在代碼量儘可能少的狀況下得出結論:
// diffX 大於0 說明是右滑,小於0 則是左滑
if (diffX > 0) {
stDirectionFlag = -1
stAutoNext = diffX > criticalWidth
toX = stAutoNext ? -clientW * (activeIndex - 1) : -clientW * activeIndex
} else if (diffX < 0) {
stDirectionFlag = 1
stAutoNext = Math.abs(diffX) > criticalWidth
toX = stAutoNext ? -clientW * (activeIndex + 1) : -clientW * activeIndex
} else {
stDirectionFlag = 0
stAutoNext = false
toX = -clientW * activeIndex
}
複製代碼
連續滑過多個 swiperItem
,則將其處理成一般狀況,也就是隻滑過最多一個 swiperItem
的狀況進行處理:
// 若是連續滑過超過一個 swiperItem 塊
if (Math.abs(diffX) > clientW) {
activeIndex = Math.ceil(-this.transX / clientW)
diffX = diffX - clientW * wholeBlock
}
複製代碼
一些移動端原生的輪播組件,都提供了一種滑動攔截的能力,具體就是,滑動一個 swiperItem
,而後手指離開,這個 swiperItem
會自動滑動到固定的位置,但你能夠經過手指觸摸或再次滑動打斷這個過程,改變 swiperItem
本來的軌跡:
大概看了下,彷佛 vue-awesome-swiper 和 vue-swipe 都沒有提供這種能力,雖然說無傷大雅,但就由於少了這一個能力,總感受就沒有原生的那種順滑的體驗,因此我決定加上
針對這個功能,一開始是想將 自動滑動 的這個動做,使用 js
來動態計算,利用 requestAnimationFrame
來模擬自動滑動的動畫效果,這樣就可以很方便地獲取任什麼時候刻 swiperItem
的 translate
數值了,接下來實現攔截的能力也就很簡單了
但後來又考慮到用 js
模擬動畫的性價比過低了,實際生產過程當中很容易碰到卡頓的狀況,因而轉向了另一種實現
自動滑動的動畫交給 css
來處理,當手指觸摸正在滑動中的 swiperItem
時,經過 getBoundingClientRect API
獲取實時位置
getBoundingClientRect API
的兼容性已經很好了,用於實際生產環境基本上沒什麼問題,不過考慮到不管怎麼說,也仍是會有一些老舊設備不支持這個 API
,因此我也作了降級處理:
const isSupportGetBoundingClientRect = typeof document.documentElement.getBoundingClientRect === 'function'
// ...
if (this.isTransToX) {
if (!isSupportGetBoundingClientRect) {
return touchStatus = 0
}
this.isTransToX = false
this.transX = stPrevX = this.$refs.sliderWrapper.getBoundingClientRect().left - this.$refs.swiperContainer.getBoundingClientRect().left
}
複製代碼
在冒出要本身動手造輪子的念頭時候,以爲這個輪子沒什麼難度,快的話一天慢點三天也差很少了,然而真正開始動手開發的時候,才發現沒那麼簡單,由於只有工做之餘纔有時間作這個東西,因此最終愣是搗鼓了一星期都還沒搞定,主體部分的代碼很快寫完,但解決各類異常狀況和自測卻佔據了絕大部分的時間,不過無論怎麼說,最終仍是作完了