手把手带你造swiper

前言

作为前端技术人员,我们一开始使用着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功能:

dom.png

然后使用它去重构上面的代码,具体可参看我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就大功告成了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,312评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,578评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,337评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,134评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,161评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,303评论 1 280
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,761评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,421评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,609评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,450评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,504评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,194评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,760评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,836评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,066评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,612评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,178评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,313评论 25 707
  • 好的家风,就像家的主旋律:就像家的柱子:就像春雨,滋润着我们才能健康成长,现在我来介绍一下我们家的家风吧! 有次考...
    A萍姐阅读 319评论 0 0
  • 291976-陈国艳《2017-5-29》 【连续第107天总结】 A、目标完成情况 1、抄写概念一遍完成100%...
    国艳更文的365天阅读 93评论 0 0