簡易版Swiper是怎麼煉成的

我叫呂胖胖發表於2017-12-19

效果預覽地址

github原始碼地址

先來2張圖

簡易版Swiper是怎麼煉成的

簡易版Swiper是怎麼煉成的

淘寶京東這種購物商城H5站不可缺少的就是輪播外掛(元件是更加全面的外掛),而這次也是本胖自己都記不清是第幾次寫一個移動端輪播外掛。

寫一個外掛之前,我們要做的就是分析,不是有句話,70%的時間在思考,30%的時間在敲程式碼。多思考,多分析就能少寫程式碼,少走彎路。

1.需求分析(該外掛要實現的功能):

A.外掛容器能根據使用者手指行為而滑動

B.無縫滑動,就是能一直往一個方向滑動

C.懶載入除了第一張以後的所有圖片

D.自動播放

主要需求點就是上面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 );
    }
},
複製程式碼

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

本文完

相關文章