效果预览地址html
github源码地址git
先来2张图github
淘宝京东这种购物商城H5站不可缺乏的就是轮播插件(组件是更加全面的插件),而此次也是本胖本身都记不清是第几回写一个移动端轮播插件。web
写一个插件以前,咱们要作的就是分析,不是有句话,70%的时间在思考,30%的时间在敲代码。多思考,多分析就能少写代码,少走弯路。数组
1.需求分析(该插件要实现的功能):浏览器
A.插件容器能根据用户手指行为而滑动bash
B.无缝滑动,就是能一直往一个方向滑动babel
C.懒加载除了第一张之后的全部图片app
D.自动播放dom
主要需求点就是上面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 );
}
},
复制代码
本篇文章没有什么技术难点,只是对本身造轮子的过程的记录以及对一个插件是怎么炼成的总结
本文完