手把手教你用原生js來寫一個swiper滑塊插件(上)原理

嗨~ 這里是芝麻,今天我們一塊來做一個“滑塊插件”。那么啥是滑塊插件呢?滑塊插件能干嘛呢?請看下圖:




是不是有點印象了,沒錯,他的最基本的用法就是左右滑動,插件使用者只需要寫幾行簡單的html和js即可實現(xiàn)一個簡單滑動效果,不過你完全可以組合各種元素來適應(yīng)不同的場景。

當(dāng)然插件我已經(jīng)寫好了,咱先看下這個插件是怎么來用的,對插件有一個大概了解,一會寫起來不至于太懵逼。。。

插件地址:https://github.com/laravuel/swiper.git
demo目錄有演示和用法,不過插件我用了webpack和babel轉(zhuǎn)碼,可以不用管,直接看src/swiper.js即可。

<!-- demo.html -->

<!-- swiper名稱可以自定義的啦 -->
<div id="swiper">
    <!-- swiper-item名稱也可以自定義啦,相當(dāng)于一個滑塊 -->
    <div class="swiper-item">
        <img src="./images/1.jpg" />
    </div>
    <div class="swiper-item">
        <img src="./images/2.jpg" />
    </div>
    <div class="swiper-item">
        <img src="./images/3.jpg" />
    </div>
</div>

<script src="../dist/swiper.js"></script>
<script>
    new Swiper({
        swiper: '#swiper',      // swiper節(jié)點名稱
        item: '.swiper-item',   // swiper內(nèi)部滑塊的節(jié)點名稱
        autoplay: false,        // 是否自動滑動
        duration: 3000,         // 自動滑動間隔時間
        change(index) {         // 每滑動一個滑塊,插件就會觸發(fā)change函數(shù),index表示當(dāng)前的滑塊下標
            console.log(index);
        }
    });
</script>

就是這么簡單,插件本身只是一個類,你只需要new一個對象出來,然后傳遞一些參數(shù)就ok了。而且,插件還提供了一個change方法,讓使用者可以在外部控制滑塊的滑動!

const swiper = new Swiper({...});
swiper.change(2); // 滑動到第三個滑塊

那么接下來,就是我們的教程時間了,我也不確定你能不能硬著頭皮看完,不過我敢肯定,如果你能夠親手把插件寫出來,你肯定會開心的飛起?。?!

由于本次教程內(nèi)容比較多,所以我分上下兩部分來講,第一部分主要講解原理,第二部分開始著手編寫插件。所以,感興趣的小伙伴可以加個關(guān)注先。


1. 功能分析

俗話說,一上來就貼代碼純屬耍流氓~
我們要清楚自己想實現(xiàn)哪些功能,懶得思考的童鞋可以結(jié)合我上面的動圖來分析:

  1. 滑塊可以左右滑動(支持移動端和pc端)
  2. 滑塊塊內(nèi)部可以寫任何元素
  3. 滑動到第一個和最后一個滑塊時會有一個限制,防止越界
  4. 能夠自動播放

我們所能看到的大概就這些,接下來我們會對這些功能一一進行拆解和分析。


2. 實現(xiàn)原理

上面簡單梳理了一些功能,其實可以再擴展出以下幾個問題:

  1. 滑塊的html結(jié)構(gòu)是什么樣的?
  2. 滑塊的滑動原理是什么?
  3. 如何來觸發(fā)滑動?

別急,一個個來

2.1.1 滑塊的html結(jié)構(gòu)是什么樣的?

我們先來看一張圖:


滑塊結(jié)構(gòu)

這就是一個滑塊的最基本的結(jié)構(gòu)圖,有三個部分組成:

  • 視圖
    我們的內(nèi)容展示區(qū)域,相當(dāng)于最外層的一個展示層
  • 容器
    容器的寬度是無限長的,容納我們所有需要切換的內(nèi)容,滑塊的左右滑動,實質(zhì)上是容器的左右移動(left),而每個滑塊相對于容器其實是靜止的
  • 滑塊
    一個個的內(nèi)容

那么根據(jù)這個結(jié)構(gòu),可以用如下html代碼來表示:

<!-- 視圖層 -->
<div class="swiper">
    <!-- 容器 -->
    <div class="swiper-container">
        <!-- 滑塊 -->
        <div class="swiper-item" style="background: #000">1</div>
        <div class="swiper-item" style="background: #4269eb">2</div>
        <div class="swiper-item" style="background: #247902">3</div>
    </div>
</div>

然后再配上css樣式:

.swiper {
    position: relative;
    width: 300px;
    
    /* 下面是為了讓大家看的更清楚,加的修飾 */
    padding: 30px 0;
    margin: 0 auto;
    background: #FFB973;
}
.swiper .swiper-container {
    position: relative;
    /* 為啥要設(shè)置-300px呢,因為我想讓他默認在第二個滑塊的位置,一會會給大家演示 */
    left: -300px;
    /* 讓容器盡可能的寬,這樣才能容納更多的滑塊 */
    width: 10000%;
    /* 讓內(nèi)部滑塊可以排成一行 */
    display: flex;

    /* 下面是為了讓大家看的更清楚,加的修飾 */
    background: red;
    padding: 15px 0;
}
.swiper .swiper-container .swiper-item {
    /* 寬度設(shè)置1%會按照外層視圖的寬度來鋪滿 */
    width: 1%;
    height: 300px;
    background: #eee;

    /* 下面是為了讓大家看的更清楚,加的修飾 */
    text-align: center;
    font-size: 40px;
    color: #fff;
}


你就會看到這么個效果:


初始效果

當(dāng)然,你可以把我加的修飾css樣式都給去掉,然后再試試。


2.1.2 滑塊的滑動原理是什么?

如果你能夠理解上面的html結(jié)構(gòu)的話,那我們就可以進行滑動的講解了。(如果還不理解的話,那就繼續(xù)往下看吧~或許會突然恍然大悟?。?/p>

上面我們提到了“滑塊容器”這個概念,滑塊的左右移動就是他來負責(zé)的

.swiper .swiper-container {
    position: relative;
    left: -300px;
}

因為滑塊的寬度是和視圖的寬度一樣的,所以我們這里滑塊的寬度是300px,那么我們把容器的left設(shè)置為-300px,就相當(dāng)于向左移動了一個滑塊的寬度,設(shè)置為-600px就表示向左移動了兩個滑塊的寬度,懂了吧,如果你想移動到某個滑塊,那么只需要知道這個滑塊的順序(從0開始),然后乘以滑塊寬度的相反數(shù)就行了,比如要移動到第三個滑塊,他的順序是2,那么就是2 * -300 = -600

看下面動圖演示:


滑動原理


但是好像并沒有出現(xiàn)滑動的動畫效果耶,廢話,還沒寫呢,有些童鞋可能喜歡用jquery,習(xí)慣了他的animate動畫方法,說實話其實我不太喜歡,因為我覺得css自帶的動畫完全可以解決大部分需求,而且當(dāng)你以后用了vue這種mvvm框架,你會發(fā)現(xiàn)jquery這種動畫方式很不實用!

扯遠了,不過今天我們不用css的animation屬性,我們用另外一個屬性transition就可以滿足,看名字你也能猜到,就是一個過渡屬性,詳細的用法請參考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions

我們把容器加上transition屬性試試看哈:

.swiper .swiper-container {
    /* 省略... */
    
    transition: left 0.2s ease-in-out;
}
滑動動畫



所以,我們寫的這段css代碼transition: left 0.2s ease-in-out;是表示:如果元素的left值變更,那么會有一個0.2s的過渡動畫(補間動畫)

到這里,我覺得你應(yīng)該能理解了吧,每個滑塊swiper-item的左右滑動,并不是滑塊本身在移動,而是他的父元素swiper-container容器在左右移動(left值變化),然后我們用transition屬性來讓這個變化過程出現(xiàn)一個過渡動畫效果!


2.1.3 如何來觸發(fā)滑動?

上面我們扯了一堆html和css,接下來我們說點js吧。
“如何來觸發(fā)滑動?”,我們先不考慮手機端,就按照pc網(wǎng)頁來,那么觸發(fā)操作就是在容器上按住鼠標向左/右拖動,然后松開鼠標后,滑塊就會向左/右滑動

整個流程都跟鼠標事件掛鉤:

  1. mousedown 鼠標按下事件
  2. mousemove 鼠標移動事件
  3. mouseup 鼠標抬起事件

利用好這3個事件,我們就可以來實現(xiàn)鼠標控制滑塊移動了!!我們先來實現(xiàn)摁住鼠標向左、向右拖動滑塊。
既然我們的容器swiper-container是負責(zé)左右移動的,那么我們就來監(jiān)聽他的鼠標事件吧,首先用querySelector獲取視圖容器兩個元素節(jié)點:

// 首先獲取視圖層元素
const swiperEl = document.querySelector('.swiper');
// 在視圖層里邊查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container');

獲取到容器元素后,就可以用他的addEventListener來監(jiān)聽事件了:

containerEl.addEventListener('mousedown', (event) => {
    console.log('鼠標按下了');
});
containerEl.addEventListener('mousemove', (event) => {
    console.log('鼠標移動了');
});
containerEl.addEventListener('mouseup', (event) => {
    console.log('鼠標抬起了');
});


看下動圖操作:

鼠標按下抬起事件

雖然我們可以成功的監(jiān)聽到鼠標的操作事件,但是好像有點問題,我們期望的結(jié)果是,只有當(dāng)鼠標按下后才會觸發(fā)鼠標移動操作,但是現(xiàn)在看來并沒有,所以可以考慮加一個狀態(tài)來控制。

let state = 0;  // 鼠標默認狀態(tài)

containerEl.addEventListener('mousedown', (event) => {
    state = 1;  // 設(shè)置為1表示按下了鼠標
    console.log('鼠標按下了');
});
containerEl.addEventListener('mousemove', (event) => {
    if (state != 1) return; // 只有當(dāng)state == 1時候才允許執(zhí)行該事件
    console.log('鼠標移動了');
});
containerEl.addEventListener('mouseup', (event) => {
    state = 0;  // 恢復(fù)默認狀態(tài)
    console.log('鼠標抬起了');
});
鼠標按下抬起事件2

這樣就好多了!?。?/p>

那么鼠標事件有了,接下來要讓容器跟著鼠標左右動才行。

我們要知道,瀏覽器對于鼠標的任何操作,都會有一個坐標參數(shù)(pageX和pageY),所以,我們可以根據(jù)鼠標移動時候的坐標參數(shù)來計算容器的left值,你可以想象一下,當(dāng)你摁下鼠標然后左右移動,鼠標每次移動相對于上次都會產(chǎn)生一個距離,我們是不是可以把容器的left值加上或者減去這個距離,從而達到一個拖動效果呢?記得前面我們回調(diào)函數(shù)里邊的event參數(shù)了嗎,他就是鼠標當(dāng)前操作的相關(guān)屬性,而我們目前只需要用到pageX屬性

event



下面我們來寫代碼,有個地方需要注意下,我們先把容器的transition這個屬性給注釋掉,后面會解釋為什么?

.swiper .swiper-container {
    /* 省略... */
    
    /* transition: left 0.2s ease-in-out; */
}

每一步的操作,都在注釋里邊詳細標注:

// 首先獲取視圖層元素
const swiperEl = document.querySelector('.swiper');
// 在視圖層里邊查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container');

let state = 0;          // 鼠標默認狀態(tài)
let oldEvent = null;    // 用來記錄鼠標上次的位置
// 獲取容器的初始left值
let left = containerEl.offsetLeft;

containerEl.addEventListener('mousedown', (event) => {
    state = 1;  // 設(shè)置為1表示按下了鼠標
    oldEvent = event;   // 當(dāng)鼠標按下時候記錄初始位置
    console.log('鼠標按下了');
});

containerEl.addEventListener('mousemove', (event) => {
    if (state != 1) return; // 只有當(dāng)state == 1時候才允許執(zhí)行該事件

    // 用當(dāng)前鼠標的位置來和上次鼠標的位置作比較
    // 如果當(dāng)前鼠標的pageX小于上次鼠標的pageX,那就表示鼠標在向左拖動,就需要把容器left值減去鼠標移動的距離
    if (event.pageX < oldEvent.pageX) {
        left -= oldEvent.pageX - event.pageX;
    }
    else {
        left += event.pageX - oldEvent.pageX;
    }
    // 完事之后記得把當(dāng)前鼠標的位置賦值給oldEvent
    oldEvent = event;
    // 最后再把left賦值給容器
    containerEl.style.left = left + 'px';
    console.log('鼠標移動了');
});

containerEl.addEventListener('mouseup', (event) => {
    state = 0;  // 恢復(fù)默認狀態(tài)
    console.log('鼠標抬起了');
});

運行看效果:

鼠標拖動.gif



沒毛病,你看這個鼠標,他又白又。。。

可是,可是,你這鼠標松開后,也沒滑動到對應(yīng)位置啊,額,額,前面我們不是講了嘛,滑塊順序、滑塊寬度還記得么?0 - 滑塊順序 * 滑塊寬度就會移動到這個滑塊,還記得不?

我們用index來記錄當(dāng)前滑塊的順序

let index = 0;          // 記錄當(dāng)前滑塊的順序(從0開始)

itemWidth來存儲滑塊的寬度

// 獲取到所有的滑塊元素
const itemEls = containerEl.querySelectorAll('.swiper-item');
// 獲取到滑塊的寬度
const itemWidth = itemEls[0].offsetWidth;

把我們的left變量改一下,之前left變量是直接獲取容器元素的left值,現(xiàn)在我們要根據(jù)index來計算

// let left = containerEl.offsetLeft;
// 存儲容器的left,這里我們根據(jù)index來計算初始容器的left值
let left = 0 - itemWidth * index;
// 設(shè)置容器的初始位置
containerEl.style.left = left + 'px';

這樣我們只需要修改index變量的值,那么容器初始位置就會發(fā)生變化。

然后,我們在鼠標按下的時候,記錄下坐標位置,在鼠標抬起的時候拿當(dāng)前鼠標的位置和按下的位置作比較,來判斷用戶是向左劃的,還是向右劃的!


滑塊左右移動判斷

加一個變量,用來記錄鼠標按下的參數(shù),并且在鼠標按下的時候進行賦值!

let startEvent = null;  // 用來記錄鼠標按下時候的位置(最初位置)

containerEl.addEventListener('mousedown', (event) => {
    state = 1;  // 設(shè)置為1表示按下了鼠標
    startEvent = oldEvent = event;   // 當(dāng)鼠標按下時候記錄初始位置
    console.log('鼠標按下了');
});

那么鼠標抬起的時候,只需要和startEvent.pageX做比較,就可以判斷出左滑還是右滑,左滑我們讓index + 1,右滑就讓index - 1,最終我們通過index再來計算left

containerEl.addEventListener('mouseup', (event) => {
    state = 0;  // 恢復(fù)默認狀態(tài)
    
    // 鼠標抬起時候,和按下的坐標作比對,用來判斷是向左滑動還是向右滑動
    // 向左滑動那么就是要顯示下一個滑塊,所以index要加1
    if (event.pageX < startEvent.pageX) {
        index ++;
    }
    else {
        index --;
    }

    left = 0 - itemWidth * index;
    containerEl.style.left = left + 'px';
    console.log('鼠標抬起了');
});
通過index設(shè)置滑塊位置.gif



是不是像那么回事了,不過怎么沒滑動動畫呢,還記得我們注釋掉的那個transition么,為什么要注釋掉呢,因為只有在鼠標抬起的那一刻才需要滑動動畫,左右拖動是根據(jù)鼠標位移距離來計算left,數(shù)值很小,完全不需要銜接動畫,所以,我們先把注釋掉那個transition代碼單獨提取出來放到一個和swiper-container同級的.move類里邊,當(dāng)鼠標抬起的時候,我們把swiper-container追加一個move類就行。

.swiper .swiper-container.move {
    transition: left 0.2s ease-in-out;
} 
containerEl.addEventListener('mouseup', (event) => {
    state = 0;  // 恢復(fù)默認狀態(tài)
    
    // 鼠標抬起時候,和按下的坐標作比對,用來判斷是向左滑動還是向右滑動
    // 向左滑動那么就是要顯示下一個滑塊,所以index要加1
    if (event.pageX < startEvent.pageX) {
        index ++;
    }
    else {
        index --;
    }

    // 追加一個move樣式
    containerEl.className += ' move';
    // 當(dāng)過度動畫結(jié)束后,一定要把這個類給移除掉
    containerEl.addEventListener('transitionend', () => {
        // 正則替換 \s+ 表示一個或多個空白字符
        containerEl.className = containerEl.className.replace(/\s+move/, '');
    })
    
    left = 0 - itemWidth * index;
    containerEl.style.left = left + 'px';
    console.log('鼠標抬起了');
});

注意觀察swiper-container的dom節(jié)點:

滑動雛形.gif



仔細看上面的動圖,第一個和最后一個滑動的時候是不是越界了,那么我們只需要判斷index就行,看代碼:

containerEl.addEventListener('mouseup', (event) => {
    state = 0;  // 恢復(fù)默認狀態(tài)
    
    // 鼠標抬起時候,和按下的坐標作比對,用來判斷是向左滑動還是向右滑動
    // 向左滑動那么就是要顯示下一個滑塊,所以index要加1
    if (event.pageX < startEvent.pageX) {
        index ++;
    }
    else {
        index --;
    }

    // 防止滑塊越界
    // 如果當(dāng)前滑塊是第一個,向右滑動后,回到第一個滑塊
    // 如果是最后一個,向左滑動后,回到最后一個滑塊
    if (index < 0) {
        index = 0;
    }
    else if (index > itemEls.length - 1) {
        index = itemEls.length - 1;
    }

    // 追加一個move樣式
    containerEl.className += ' move';
    // 當(dāng)過度動畫結(jié)束后,一定要把這個類給移除掉
    containerEl.addEventListener('transitionend', () => {
        // 正則替換 \s+ 表示一個或多個空白字符
        containerEl.className = containerEl.className.replace(/\s+move/, '');
    })

    left = 0 - itemWidth * index;
    containerEl.style.left = left + 'px';
    console.log('鼠標抬起了');
});
image




我們擴展出的三個問題,基本上都解決了。
而且到目前為止,其實你已經(jīng)實現(xiàn)了一個基本的滑塊功能了,只不過略顯粗糙!


2.2 自動播放的原理

回到我們的功能列表,我們來看下第四條“自動播放”,第一個想到的是setInterval

setInterval(() => {
    // 這個回調(diào)會每隔2秒執(zhí)行一次
}, 2000);

所以,我們只需要在這個回調(diào)函數(shù)里邊寫上讓滑塊滑動的代碼不就行了?
我們是用index變量來控制當(dāng)前滑塊的,那么每隔2秒讓index加1,最后再根據(jù)index計算出left的值,不就可以了?

setInterval(() => {
    // 默認向左滑動
    index ++;
    // 如果滑動到最后一個滑塊,則回到第一個滑塊
    if (index > itemEls.length - 1) {
        index = 0;
    }

    // 下面的代碼跟我們鼠標抬起的事件的代碼一樣的,要不要考慮簡單的封裝一下?
    // 追加一個move樣式
    containerEl.className += ' move';
    // 當(dāng)過度動畫結(jié)束后,一定要把這個類給移除掉
    containerEl.addEventListener('transitionend', () => {
        // 正則替換 \s+ 表示一個或多個空白字符
        containerEl.className = containerEl.className.replace(/\s+move/, '');
    })

    left = 0 - itemWidth * index;
    containerEl.style.left = left + 'px';
}, 2000);
自動播放.gif



關(guān)于重復(fù)邏輯的問題,我們會在第二部分寫插件時候進行封裝,這部分,我們只講原理,當(dāng)然如果你是個強迫癥患者,可以自己試著封裝個函數(shù)。

不過他老是這么自動播放也不是個事,有時候我想看看內(nèi)容,還沒看完呢,就自動劃走了,所以,我們可以當(dāng)鼠標放在容器上的時候,停止播放,鼠標移開后又恢復(fù)自動播放

  1. mouseover 鼠標移動到某個元素上
  2. mouseout 鼠標在某個元素上移開

我們還是在容器上監(jiān)聽這兩個事件,并用一個狀態(tài)autoplay來控制播放:

// 自動播放狀態(tài)
let autoplay = true;

setInterval(() => {
    if (!autoplay) return;  
    // 默認向左滑動
    index ++;
    // 如果滑動到最后一個滑塊,則回到第一個滑塊
    if (index > itemEls.length - 1) {
        index = 0;
    }

    // 追加一個move樣式
    containerEl.className += ' move';
    // 當(dāng)過度動畫結(jié)束后,一定要把這個類給移除掉
    containerEl.addEventListener('transitionend', () => {
        // 正則替換 \s+ 表示一個或多個空白字符
        containerEl.className = containerEl.className.replace(/\s+move/, '');
    })

    left = 0 - itemWidth * index;
    containerEl.style.left = left + 'px';
}, 2000);

containerEl.addEventListener('mouseover', () => {
    // 鼠標移動到容器上,停止播放
    autoplay = false;
});
containerEl.addEventListener('mouseout', () => {
    // 鼠標從容器上移開,恢復(fù)播放
    autoplay = true;
});
鼠標控制自動播放.gif



當(dāng)然,還有其他的方法來控制自動播放,比如用clearInterval函數(shù)等。


3. 結(jié)尾

至此,我們的原理都講的差不多了,有遺漏的地方,還望指出,那么在第二部分,我會和大家一塊來把寫的雜七雜八的代碼做一個封裝,讓我們的代碼插件化,適應(yīng)更多的場景。


喜歡的童鞋,粉一下唄~,不定時各種技術(shù)教程更新

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容