先来2张图


淘宝京东这种购物商城H5站不可缺少的就是轮播插件(组件是更加全面的插件),而这次也是本胖自己都记不清是第几次写一个移动端轮播插件。
写一个插件之前,我们要做的就是分析,不是有句话,70%的时间在思考,30%的时间在敲代码。多思考,多分析就能少写代码,少走弯路。
1.需求分析(该插件要实现的功能):
A.插件容器能根据用户手指行为而滑动
B.无缝滑动,就是能一直往一个方向滑动
C.懒加载除了第一张以后的所有图片
D.自动播放
主要需求点就是上面3点,下面就让我们一步一步来实现。
2.代码组织
用js写一个插件其实就是实现一个class,这次由于是需要兼容低端机并且不想通过babel,所以本胖是用ES5的方式组织代码的,用的是组合模式。也就是把插件需要的所有变量写函数内部,把插件里面的所有共享的方法写该函数的prototype上面。(这种模式下将一个ES5插件转为ES6插件只需要1分钟即可)。下面是这个插件最初的模子。
function Swiper(dom, options) {
this.dom = dom;
this.options = options;
this.init();}
Swiper.prototype = { init : function(){}
};
3.功能实现
A.需要设置的参数
我们可以想象一下这个插件需要哪些内部参数变量(这里本胖感觉就是需要观察和经验的地方,这种能力是需要靠写代码来培养的),下面是本胖认为这个插件需要的内部参数。每个参数都有注释哈。
this.dom = dom;
// 包裹整个插件的容器
this.swiperDom = document.querySelector( dom + ' .swiper-wrapper' );
// 容器宽度
this.winWidth = this.swiperDom.clientWidth;
this.options = options || {};
// sliding块数组,这里要在确定的容器下面查找,否则会出现多余的dom结构,当一个页面有多个该插件调用的时候
this.slidingList = this.swiperDom.querySelectorAll( '.swiper-slide' );
// 圆点容器
this.pagination = document.querySelector( dom + ' .pagination' );
// 整个容器每次开始滑动的translateX
this.startLeft = 0;
// 整个容器每次结束滑动的translateX
this.endLeft = 0;
// 每次手指开始滑动时候距离屏幕左边的距离(不包含滚动条距离,下同)
this.startX = 0;
// 每次手指开始滑动时候距离屏幕上边的距离
this.startY = 0;
// 判断该次滑动是否是横向滑动
this.swipeX = true;
// 判断该次滑动是否是轴向滑动
this.swipeY = true;
// 圆点domList
this.paginationList = null;
// 当前显示的index
this.index = 1;
// banner总数
this.num = this.slidingList.length;
this.reg = /\-?[0-9]+/g;
// 每次手指开始触摸屏幕的时间点
this.startTime = 0;
// 每次手指离开屏幕的时间点
this.endTime = 0;
// 判断一次滑动是否完整结束,可以防止用户滑动过快导致一些bug
this.oneEnd = true;
// 定时器
this.timer = null;
// 是否第一次
this.isFirst = true;
B.插件容器能根据用户手指行为而滑动
很显然,我们需要借助浏览器给我们的3个事件
touchstart,touchmove,touchend 既然是事件的话,那么我们就需要绑定,那么这3个事件的绑定一定是在上面的init函数里面。
// 绑定手指触摸事件
this.swiperDom.addEventListener('touchstart', function(event) {
if ( this.oneEnd ) {
this.startListener(event);
}
}.bind(this));
// 绑定手指滑动事件
this.swiperDom.addEventListener('touchmove', function(event) {
if ( this.oneEnd ) {
this.moveListener(event);
}
}.bind(this));
// 绑定手指结束滑动事件
this.swiperDom.addEventListener('touchend', function(event) {
this.endListener(event);
}.bind(this));
上面用了bind函数来避免大量使用var oThis = this;这种代码。
接下来就是实现
this.startListener(),this.moveListener(),this.endListener()这3个事件方法。
this.startListener():
// touchstart事件
startListener : function(event) {
var target = event.targetTouches[0];
// 禁止自动播放(如果设置了定时器时间间隔)
clearInterval(this.timer);
// 获取当前时间,后面用来判断是否点滑需要用到
this.startTime = (new Date()).getTime();
// 记录当前滑动容器的translate3d值
this.startLeft = parseFloat(this.swiperDom.style.webkitTransform.match(this.reg)[1]);
this.startX = target.pageX;
this.startY = target.pageY;
},
该方法的作用是获取用户手指一开始在屏幕的位置以及touchstart事件触发的时候当前容器的translate3d值(本胖是通过改变translate3d来让容器滑动的)
注意这里获取了一个touchstart事件触发的时刻,是用来判断是否需要触发点滑事件的。
this.moveListener():
// touchmove事件
moveListener : function(event) {
var target = event.targetTouches[0];
this.moveX = target.pageX;
this.moveY = target.pageY;
// 判断是X轴滑动
if ( this.swipeX && this.cal(this.startX, this.startY, this.moveX, this.moveY) ) {
this.swipeY = false;
var x = parseFloat(this.startLeft + this.moveX - this.startX);
this.swiperDom.style.webkitTransform = 'translate3d('+ x +'px,0px,0px)';
} else {
this.swipeX = false;
}
},
touchmove事件需要做的事情就是判断当前用户手指的意图是不是想沿X轴,本胖用了this.cal(this.startX, this.startY, this.moveX, this.moveY)才判断用户意图。
this.endListener():
// touchend事件
endListener : function (event) {
// 重新开启自动播放(如果设置了定时器时间间隔)
this.setTimer();
this.oneEnd = false;
// 获取当前时间,后面用来判断是否点滑需要用到
this.endTime = (new Date()).getTime();
this.endLeft = this.getTranslate3d();
// 滑动距离
var distance = Math.abs(this.endLeft - this.startLeft),
halfWinWith = this.winWidth/2,
left = this.startLeft;
// 手指接触屏幕时间大于300ms,开启点滑效果
if ( this.endTime - this.startTime <= 300 ) {
halfWinWith = 30;
}
if ( this.endLeft <= this.startLeft ) {
// 向左滑动 未过临界值
if ( distance <= halfWinWith ) {
left = this.startLeft;
} else {
left = this.startLeft - this.winWidth;
}
} else {
// 向右滑动 未过临界值
if ( distance <= halfWinWith ) {
left = this.startLeft;
} else {
left = this.startLeft + this.winWidth;
}
}
this.swiperDom.style.webkitTransition = 'transform 300ms';
this.swiperDom.style.webkitTransform = 'translate3d('+ left +'px,0px,0px)';
// 触发动态滑动结束事件
this.transitionEndListener();
},
这个事件是该插件的重点,里面获取了手指离开屏幕的时间,以及通过用户已经滑动的距离来设置容器最终的滑动距离,这里的规则是如果时间间隔在0-300ms之内(表现为用户在短时间手指划过,单手操作的时候很容易发生这种现象),并且容器滑动的距离比30px大,那么就认为用户想换一张图片,否则容器还原。如果时间间隔大于300ms,并且容器滑动距离比容器的可视宽度一般多,那么也认为用户想换一张图片,否则容器还原。这里判断是否切换的逻辑和淘宝首页banner是一样的,其实还可以有很多哈。
C.无缝滑动,就是能一直往一个方向滑动
本胖这里是在容器最前面和最后面都加了一个dom,最前面加的是最后面的dom,最后面加的是最前面的dom,代码如下
// 克隆收尾的图片结构,为无缝轮播做准备
var firstNode = this.slidingList[0].cloneNode(true),
lastNode = this.slidingList[this.num - 1].cloneNode(true),
oFrag = document.createDocumentFragment();
this.swiperDom.insertBefore(lastNode, this.slidingList[0]);
this.swiperDom.appendChild(firstNode);
this.swiperDom.style.webkitTransform = 'translate3d('+-this.winWidth+'px,0px,0px)';
this.slidingList = document.querySelectorAll( this.dom + ' .swiper-slide');
然后就是一开始用户看到的是实际第二个dom(这个dom本来是index=0,由于在最前面加了一个dom,所以就变成了index=1)
然后就是每次滑动过后在最前面和最后面做一个判断
// 动态滑动结束事件
transitionEndListener : function() {
this.isFirst = false;
this.swiperDom.addEventListener("webkitTransitionEnd", function() {
this.oneEnd = true;
this.swiperDom.style.webkitTransition = 'transform 0ms';
// 重新计算当前index
this.index = -(this.getTranslate3d())/this.winWidth - 1;
// 对2种临界状态做一个判断
if( this.index===-1 ) {
this.index = this.num-1;
this.swiperDom.style.webkitTransform = 'translate3d('+ (-this.winWidth * (this.num)) +'px,0px,0px)';
}
if( this.index>=this.num ) {
this.index = 0;
this.swiperDom.style.webkitTransform = 'translate3d('+ -this.winWidth +'px,0px,0px)';
}
this.lazyPlay(this.index+1);
// 给pagination里面的圆点添加对应样式
for(var i=0; i<this.num; i++) {
this.paginationList[i].className = 'swiper-pagination-bullet';
}
this.paginationList[this.index].className = 'swiper-pagination-bullet swiper-pagination-bullet-active';
}.bind(this), false);
},
对了这里的滑动动画本胖是用了webkitTransition,所以可以在webkitTransitionEnd事件里面判断一次滑动是否结束即可。
D.懒加载除了第一张以后的所有图片
这里的思路和其他图片懒加载插件一样,就是一开始不给图片设置真实的src,而是把图片地址放在data-src里面,然后在适当的时机去加载正式的图片即可。(懒加载的思想很重要)
// 如果开启了懒加载模式
lazyPlay : function(index) {
if ( this.options.lazyLoading ) {
var slidingDom = this.slidingList[index];
imgDom = slidingDom.querySelector('img'),
lazyDom = slidingDom.querySelector('.swiper-lazy-preloader');
if ( imgDom.getAttribute('data-src') ) {
imgDom.src = imgDom.getAttribute('data-src');
imgDom.removeAttribute('data-src');
if ( lazyDom ) {
slidingDom.removeChild(lazyDom);
}
}
// 如果是第一个则将最后一个由第一个克隆的也转化
if ( index === 1 ) {
this.lazyPlay(this.num+1);
}
// 如果是最后一个则将第0个由第this.num-1个克隆的也转化
if ( index === this.num ) {
this.lazyPlay(0);
}
}
},
E.自动播放
这个就简单了,设置一个定时器即可,在手指移入的时候清空这个定时器,手指移开的时候重新开始计时就可以了。
// 自动轮播
autoMove : function() {
this.isFirst ? this.index++ : this.index= this.index + 2;
this.swiperDom.style.webkitTransition = 'transform 300ms';
this.swiperDom.style.webkitTransform = 'translate3d('+ (-this.index * this.winWidth) +'px,0px,0px)';
this.transitionEndListener();
},
// 自动轮播定时
setTimer : function() {
if ( this.options.autoplay >= 1000 ) {
this.timer = setInterval(function() {
this.autoMove();
}.bind(this), this.options.autoplay );
}
},
本篇文章没有什么技术难点,只是对自己造轮子的过程的记录以及对一个插件是怎么炼成的总结
本文完