簡易版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 );
    }
},
複製代碼

本篇文章沒有什麼技術難點,只是對本身造輪子的過程的記錄以及對一個插件是怎麼煉成的總結

本文完

相關文章
相關標籤/搜索