图片轮播是种很常见的场景和功能,通常移动网站首页的轮播 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
}
复制代码
在冒出要本身动手造轮子的念头时候,以为这个轮子没什么难度,快的话一天慢点三天也差很少了,然而真正开始动手开发的时候,才发现没那么简单,由于只有工做之余才有时间作这个东西,因此最终愣是捣鼓了一星期都还没搞定,主体部分的代码很快写完,但解决各类异常状况和自测却占据了绝大部分的时间,不过无论怎么说,最终仍是作完了