前言
作为前端技术人员,我们一开始使用着jquery做各种dom操作,使用基于它的插件例如nicescroll、pagination等等做各种特殊功能处理,后来业界又出现了backbone、angularjs、vue、react等等各种库,给我们的开发带来无尽的便利,但对这些库或框架使用得越多,你有没有感觉自己的底层能力越来越弱?对基本js的驾驭能力越来越力不从心?
这就是过渡使用框架的结果,我自己在这方面也深有体会,需要什么直接就去github上搜索相关的库,直接引入到代码中,当长期以往,没有底层能力的支撑,就像是一栋建筑没有了地基一样,底部不稳,往上想更进一步也会越来越难。
废话了这么多,核心一点:通过造轮子去探索js底层的奥秘,学习那些优美的艺术,夯实js根基!
本篇主要实现swiper的核心功能:能让它滑动起来!
<b>完整代码可在我的github找到:</b>
https://github.com/kekobin/KSwiper
基本构型
;(function() {
var KSwiper = function KSwiper(element, options) {//且传进来的必须为DOM
if(!element) return;
this.container = element;
this.element = this.container.children[0];
this.opts = options;
this.speed = options.speed || 100;
this.index = 0;//默认开始的索引
this.callback = options.callback;
this.init();
};
if(typeof module !== 'undefined' && module.exports) {
module.exports = KSwiper;
} else if(typeof define === 'function' && (define.amd || define.cmd)) {
define(function() { return KSwiper; });
} else {
this.KSwiper = KSwiper;
}
}).call(function() {
return this || (typeof window != 'undefined' ? window : global);
}());
这个没什么好说的,不过为了简单,传进去的element必须为DOM(2.0版会增加相应DOM操作功能).
关键的初始化
这一块包括样式的初始化和事件的初始化。
html结构实例
<div class="kswiper-container">
<div class="kswiper-wrap">
<div class="kswiper-item">slide 1</div>
<div class="kswiper-item">slide 2</div>
<div class="kswiper-item">slide 3</div>
<div class="kswiper-item">slide 4</div>
</div>
</div>
样式初始化
一个swiper应该包括外部容器、内部容器、子项目,外部容器应该是overflow:hidden的,内部容器应该动态设置其宽度为 子项目宽度x子项目个数(如果子项目之间有间隙,则还应该加上该间隙,此处忽略).
this.slides = this.element.children;
this.length = this.slides.length;
this.width = ('getBoundingClientRect' in this.container) ? this.container.getBoundingClientRect().width : this.container.offsetWidth;
//设置包裹元素的宽
this.element.style.width = this.width * this.length + 'px';
//设置slide item的样式
var index = this.length;
while(index--) {
var slide = this.slides[index];
slide.style.width = this.width + 'px';
slide.style.height = '100%';
slide.style.display = 'table-cell';
}
至此,基本工作准备就绪了,接下来才是能让子项目滑动起来的关键!
事件初始化
既然是滑动,肯定要用到touch事件了。无论是左右滑动,还是上下滑动,核心是:在滑动结束后,看这次滑动是否满足一定条件(例如左右滑动时,滑动了一个子项目的一半视为有效滑动,若有效滑动则将内部容器横向移动一个子项目的宽度,否则仍回到初始状态),这是一种动态的变化过程,使用什么能做到这种动画效果呢?答案是transform或者transform3D,这种动画往往伴着着transitionEnd事件,这样我们可以在动画结束后作出回调处理。
if(this.element.addEventListener) {
this.element.addEventListener('touchstart', this, false);
this.element.addEventListener('touchmove', this, false);
this.element.addEventListener('touchend', this, false);
this.element.addEventListener('webkitTransitionEnd', this, false);
this.element.addEventListener('msTransitionEnd', this, false);
this.element.addEventListener('oTransitionEnd', this, false);
this.element.addEventListener('transitionEnd', this, false);
}
需要主意的是,这里对各种事件处理部分(即第二个参数),传入的是this,是为了简单、统一处理的目的,前提是在this对象上定义了handEvent方法:
KSwiper.prototype.handleEvent = function(event) {
var type = event.type;
switch(type) {
case 'touchstart':
this.onTouchStart(event);
break;
case 'touchmove':
this.onTouchMove(event);
break;
case 'touchend':
this.onTouchEnd(event);
break;
case 'webkitTransitionEnd':
case 'msTransitionEnd':
case 'oTransitionEnd':
case 'transitionEnd':
this.transitionEnd(event);
break;
}
};
在滑动的整个过程中,我们要做到滑动时子模块要跟着动,在滑动结束时判断是否为一次成功的滑动,成功则滑到下一个或者上一个子项目,非成功则回到滑动前的状态。
那么,在滑动开始需要处理的也很简单,只需要记录下开始滑动点的x轴和y轴的偏移量:
滑动开始
var touch = e.touches[0];
this.start = {
pageX: touch.pageX,
pageY: touch.pageY
};
//设置滑动的标识(主要从性能上考虑)
this.isScrolling = undefined;
//在每次滑动触发的开始重置element的动画的持续时间为0
var style = this.element.style;
style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.OTransitionDuration = style.transitionDuration = 0 + 'ms';
滑动时
上面说过,滑动时应该让子项目跟着动,怎么做到呢?其实也很简单,只要将滑动时的偏移量与上一步骤中初始化量做差值,动态设置内部容器在x轴上的变换即可:
if(e.touches.length > 1) return;//多指操作不认为是swipe操作
var touch = e.touches[0];
this.delta = {
x: touch.pageX - this.start.pageX,
y: touch.pageY - this.start.pageY
};
//x轴的偏移大于y轴偏于才认为是正常滑动
this.isScrolling = Math.abs(this.delta.x) > Math.abs(this.delta.y);
if(this.isScrolling) {
var duration = this.speed;
var style = this.element.style;
style.MozTransform = style.webkitTransform = 'translate3d(' + (this.delta.x - this.index * this.width) + 'px, 0, 0)';
style.msTransform = style.OTransform = 'translateX(' + (this.delta.x - this.index * this.width) + 'px)';
}
这里有个问题是,当滑动得非常快速的时候,从性能角度上讲是消耗很大的,可以给一个时间限制,超过某个时间间隔的滑动才算正常滑动,否则不做处理。
滑动结束
经过上面的滑动后,究竟这次滑动是否成功呢(这里的成功指的是成功滑动到下一个或者上一个),那么就需要在这里计算上面两个步骤的偏移差值是否符号成功滑动的条件,符合条件则将当前的索引index加1或者减1,然后调用公共滑动方法,滑动到经过计算后的index,即达到目的:
//滑动item宽度的1/6(已经足够灵敏了)即认为是有效的滑动到下一个或者上一个了,
//否则依然是当前index.
if(this.delta.x > 0 && this.delta.x > this.width / 6) {
this.index -= 1;
if(this.index < 0) this.index = 0;
}
if(this.delta.x < 0 && Math.abs(this.delta.x) > this.width / 6) {
this.index += 1;
if(this.index == this.length) this.index = this.length - 1;
}
if(this.isScrolling) this.slideTo(this.index);
由步骤二滑动时的处理可看到slideTo方法就很简单了,即变换到内部容器的-(index * width)即可:
duration = duration || this.speed;
var gap = index % this.length;
if(index >= this.length) index = gap;
if(index < 0) index = this.length + gap - 1;
var style = this.element.style;
style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.OTransitionDuration = style.transitionDuration = duration + 'ms';
style.MozTransform = style.webkitTransform = 'translate3d(' + -(index * this.width) + 'px, 0, 0)';
这里使用index % this.length是为了兼容传入的index >= length的情况.
动画结束
在整个transform动画结束后,即可处理我们的回调,反馈整个滑动的结果:
this.callback && this.callback.call(this,this.index);
至此,一个简陋但核心功能完整的swiper就实现了.
不过,上述项目依然存在着如下几个问题:</br>
(1) 传进去的element必须为dom太不灵活</br>
(2) 没有paganation功能</br>
(3) 没有autoplay功能</br>
(4) 使用原生的写法太麻烦,也不易于多功能的扩展</br>
综上,为了完成(1)、(4)以及扩展其它更多功能,最好是避免使用原生那种写法,转而在内部构件一套简版的jquery功能:
然后使用它去重构上面的代码,具体可参看我github上的实现:
https://github.com/kekobin/KSwiper/blob/master/kswiper-0.1.1.js
pagination和autoplay的实现
有了简版jquery后,(2)、(3)的实现就简单容易多了,这部分完整的代码参见:
https://github.com/kekobin/KSwiper/blob/master/kswiper-0.1.2.js
pagination
对于(2),这是一个可配置项,当传入的参数中pagination为true时,首先根据子项目的个数初始化它:
var $wrap = $("<div class='kswiper-pagination'></div");
var child = '';
for(var i=0;i<this.length;i++) {
var className = i === 0 ? 'active' : "";
i === 0 ? child = "<a class='active' data-index='0'></a>" : child += "<a data-index="+i+"></a>";
}
$wrap.append(child);
this.$container.append($wrap);
从而动态得到类似如下的结构:
<div class="kswiper-pagination">
<a class="active" data-index="0"></a>
<a data-index="1"></a>
<a data-index="2"></a>
<a data-index="3"></a>
</div>
同时,对所有分页子元素,即上面的a标签添加点击事件,实现相关的逻辑:
if(this.pagination) {
var _this = this;
this.$container.find('.kswiper-pagination>a').on('click', function(e) {
e.preventDefault();
$(this).addClass('active').siblings().removeClass('active');
_this.slideTo($(this).attr('data-index'));
});
}
分页功能似乎完善了,但等等。。。当成功滑动到上一个或者下一个时,我们仍然需要同步更新分页选中的状态,也即上面a标签active的状态,所以在slideTo方法中需要做同步处理:
if(this.pagination) {
this.$container.find('.kswiper-pagination>a.active').removeClass('active');
this.$container.find('.kswiper-pagination>a:nth-child('+(index+1)+')').addClass('active');
}
autoplay
autoplay这个功能就是一个定时滑动而已,没什么值得深入的问题,直接上代码:
if(this.auto) {
var autoIndex = 1;
var _this = this;
this.autoTimeout = setInterval(function() {
_this.slideTo(autoIndex++);
}, this.autoDuration);
}
其中,autoDuration是定时滑动的延时时间.
至此,一个趋向完善的swiper就大功告成了!