效果預覽地址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 );
}
},
複製代碼
本篇文章沒有什麼技術難點,只是對本身造輪子的過程的記錄以及對一個插件是怎麼煉成的總結
本文完