造輪子之圖片輪播組件(swiper)

圖片輪播是種很常見的場景和功能,通常移動網站首頁的輪播 banner,商品閒情頁的商品圖片等位置都會用到此功能css

像這種經常使用的場景功能確定是有人早就寫好插件了的,因此遇到這種場景,通常都遵循如下三步:html

  • 打開冰箱 啓動 Github
  • 搜索 swipersliderAlbum等關鍵字
  • 找到想要的庫,npm install

這種作法沒毛病,有現成的輪子可用固然拿來主義,由於項目用的是 vue,因此我在網上找了一圈 基於 vue的輪播組件庫,找到了兩個比較滿意的庫:vue-awesome-swipervue-swipe前端

比較知名的輪播框架,通常都會優先使用這個庫,功能豐富,適用於各類輪播場景,什麼 左右按鈕,動態指示點、進度條指示器、垂直切換、一次性顯示多個 slides……功能簡直不要太完善 but 我只是想用其中一小部分基本功能而已,如此多的功能於我而言不只是看文檔費勁,更關鍵的是會在項目中引入太多的冗餘代碼,好不容易經過各類手段將代碼體積降下來,結果就由於引入了一個包一下回到解放前,要不得要不得vue

餓了麼前端團隊出品的一個庫,比較精簡,代碼量也不多,但又過於精簡了,例如不支持無限輪播,不支持自定義 swiperItem,並且總感受有些生硬的感受git

至於其餘本人可以搜索到的庫,都沒什麼名氣或者下載量過小,不敢輕易在生產環境引入,因而就萌生了本身造個輪子來搞定這件事,這樣組價庫的功能和代碼體積本身都能控制,就算有什麼 bug也能很快自行修正github

先看下最終實現效果:npm

或者你想本身體驗一下,這裏也有個寫好的 Demo數組

我已經將此功能打包成了一個 npm package,可直接下載安裝使用,包括樣式在內的代碼體積壓縮後不到 18KB,Gzipped以後不到 7KB源碼 已上傳瀏覽器

滑動形式

爲了描述方便,先定義一下名詞,將每個滑動小塊稱爲 swiperItem,將容納全部滑動小塊的容器稱爲 swiperapp

目前大多數的滑動組件庫,都是經過兩種方式實現組件的滑動的

第一種,同一時間只渲染三個 swiperItem,每次滑動到下一個 swiperItem以後,當即更新這三個 swiperItem

這種作法的優勢是,不管有多少個 swiperItem都不會影響到瀏覽器的渲染性能,由於不管多少個,每次都只渲染其中的三個,缺點在於若是 swiperItem的數量原本就少於三個,就須要額外的處理了,並且由於每次最多隻能滑動一個 swiperItem 的距離,使用起來不是那麼順滑,vue-swipe採用的是這種

第二種,一次性渲染全部的 swiperItem,而且有時候爲了更順滑的體驗,還會在原 swiperItem的首尾,再各添加一個 swiperItem 例如,原 swiperItem的數據爲 1, 2, 3, 4, 5,處理以後變成 5, 1, 2, 3, 4, 5, 1vue-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動態組件 firstSwiperItemlastSwiperItem,就是上面說的 5,1,2,3,4,5,1中的 51

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事件

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-swipervue-swipe 都沒有提供這種能力,雖然說無傷大雅,但就由於少了這一個能力,總感受就沒有原生的那種順滑的體驗,因此我決定加上

針對這個功能,一開始是想將 自動滑動 的這個動做,使用 js來動態計算,利用 requestAnimationFrame來模擬自動滑動的動畫效果,這樣就可以很方便地獲取任什麼時候刻 swiperItemtranslate數值了,接下來實現攔截的能力也就很簡單了

但後來又考慮到用 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
}
複製代碼

總結

在冒出要本身動手造輪子的念頭時候,以爲這個輪子沒什麼難度,快的話一天慢點三天也差很少了,然而真正開始動手開發的時候,才發現沒那麼簡單,由於只有工做之餘纔有時間作這個東西,因此最終愣是搗鼓了一星期都還沒搞定,主體部分的代碼很快寫完,但解決各類異常狀況和自測卻佔據了絕大部分的時間,不過無論怎麼說,最終仍是作完了

源碼已經放到 github上了,代碼註釋得也算是比較詳細,感興趣的能夠參考下,若是有什麼問題,歡迎提 issues

相關文章
相關標籤/搜索