造轮子之图片轮播组件(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

相关文章
相关标签/搜索