先來2張圖
淘寶京東這種購物商城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 );
}
},
複製程式碼
本篇文章沒有什麼技術難點,只是對自己造輪子的過程的記錄以及對一個外掛是怎麼煉成的總結
本文完