简易版Swiper是怎么炼成的

效果预览地址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 );
    }
},
复制代码

本篇文章没有什么技术难点,只是对本身造轮子的过程的记录以及对一个插件是怎么炼成的总结

本文完

相关文章
相关标签/搜索