【云+社区年度征文】简单的无缝轮播图

前言

轮播图出现各大网站上-无论是pc还是移动端,尤其是电商网站必然能看见轮播图,它使得用户不用滚动屏幕就能看到更多内容,也常常作为广告位。而作为一个前端工程师,手写轮播图是一个必备的技能。

下图展示了京东,淘宝,腾讯云3个网站的轮播图。最常见的2种轮播图有淡入淡出,无缝轮播。无缝轮播对于用户体验会更好一些。

京东商城
淘宝
腾讯云

实现功能

  1. 实现一个含有5张图片的无缝轮播图。
  2. 鼠标悬停在轮播图部分时,轮播图停止切换,鼠标离开继续自动切换。
  3. 通过点击左右2边的按钮,进行轮播图的前一张或后一张的切换。
  4. 在图片动画未切换完成之前,禁止切换下一张图片。

效果图如下:

无缝轮播

何为无缝

无缝轮播图,即是在图片左右切换时,最后一张和第一张相连,也就是当主屏幕显示最后一张图片时,如果用户点击下一张图片时,这时候需要将第一张图片呈现给用户。同理当目前主屏幕上显示第一张图片时,如果用户点击上一张图片时,需要将最后一张图片呈现给用户。

处理办法如下图(序号为当前编号的图片):

初始化轮播图时,我们复制第一张图片与最后一张图片,将复制好的第一张图片放在图片末尾,复制好的最后一张图片放在队列头部。这样当轮播图进行到最后一张时,我们将轮播图位置更改为初始的图片1位置。若我们向左边点击时,遇到图片5时,我们将图片拉到最后一张图片5得位置。这样就不会出现播到最后一张图片后,导致的没图片出现空白的情况。这样就是无缝轮播。

罗列难点

  1. 滚动到队列末尾时,改为队列第二张图片。
  2. 用户频繁点击切换图片,之前动画未结束造成的显示错乱。
  3. 在图片运动结束后,图片没有完全切换完成的情况。
  4. 图片运动时,等待轮播的计时器未停止。

布局

布局这一块的话,基本没有什么大问题。就直接上代码。

html部分

代码语言:javascript
复制
<div id="wrap">
        <div id="box-wrap">
            <div class="item">
                <img src="https://img10.360buyimg.com//babel/jfs/t1/147387/5/18994/136741/5fdc77afE5f82113e/f4c98e84e67f8fd0.jpg!q80.webp" alt="">
            </div>
            <div class="item">
                <img src="https://img11.360buyimg.com//babel/jfs/t1/138375/30/18878/225016/5fdcb77fE3ed18d79/71d924b7f529c6ea.jpg!q80.webp" alt="">
            </div>
            <div class="item">
                <img src="https://img12.360buyimg.com//babel/jfs/t1/144721/38/17890/108504/5fd37694E682d34fc/9d1ac8a5d13b94f8.jpg!q80.webp" alt="">
            </div>
            <div class="item">
                <img src="https://img10.360buyimg.com//babel/jfs/t1/148646/26/18903/93961/5fdc9321E51f8e513/c4324e7ea048805c.jpg!q80.webp" alt="">
            </div>
            <div class="item">
                <img src="https://img14.360buyimg.com//babel/jfs/t1/155501/29/10296/70544/5fdc8f2fE6b2fab26/5423c671aa4e21bf.jpg!q80.webp" alt="">
            </div>
        </div>
        <div class="btn">
            <div class="arrow prev" id="left-arrow"><</div>
            <div class="arrow next" id="right-arrow">></div>
        </div>
    </div>

css部分

代码语言:javascript
复制
       *{
            padding: 0;
            margin: 0;
        }
        #wrap{
            position: relative;
            width: 780px;
            height: 400px;
            margin: 50px auto;
            border: 1px solid black;
            overflow: hidden;
            cursor: pointer;
        }
        #box-wrap{
            position: absolute;
            left: 0;
            top: 0;
            display: flex;
        }
        #box-wrap .item{
            width: 780px;
            height: 400px;
        }
        .item img{
            width: 100%;
        }
        .red{
            background-color: red;
        }
        .green{
            background-color: green;
        }
        .black{
            background-color: black;
        }
        .arrow{
            position: absolute;
            top: 50%;
            width: 30px;
            height: 30px;
            transform: translate(0,-50%);
            color: rgb(201, 200, 200);
            font-size: 20px;
            text-align: center;
            line-height: 30px;
            background: rgb(0, 0, 0, .3);
        }
        .arrow:hover{
            background: rgb(0, 0, 0, 1);
            color: white;
        }
        .btn{
            user-select: none;
        }
        .btn .prev{
            left: 0;
        }
        .btn .next{
            right: 0;
        }

功能分析

如上面的代码完成布局之后,效果如下图,接下来我们就需要让图片自动轮播。

布局图

布局图

轮播逻辑

DOM加载完成之后通过setInterval、定位,让图片队列盒子#box-wrap在展示图片的盒子中进行移动,即随着时间的变化改变DOM(#box-wrap)的left值。关键代码如下。

代码语言:javascript
复制
let time = null
time = setInterval(()=>{
       nextRun()
},5000)

nextRun = () => {
if (index === itemLength - 1) { // 当图片达到最后一张时,赋值为第一张
index = 1
boxWrap.style.left = -itemWidth*index+"px" // 这里的itemWidth为单张图片宽度
}
++index
move(boxWrap, itemWidth, 15) // 调用运动函数
}

move = (el, target, speed) => {
let s = parseInt(boxWrap.style.left) // 当前图片的移动距离
let t = target/speed // 计算时间,总位移距离/单次跑的步长
let s1 = 0 // 当前初始位移

let time2 = setInterval(()=&gt;{
    s1 += speed
    el.style.left = -s1 + s + &#34;px&#34;
    
    if (s1 + speed &gt; target) {
        el.style.left = -target + s + &#34;px&#34;
        clearInterval(time2)
    }
}, t)

}

发现计算步长始终会少那么一点点,最后需要补齐,我们把最后一个参数修改为总时间t,这样时间是会减少到0的。

代码语言:javascript
复制
nextRun = () => {
if (index === itemLength - 1) { // 当图片达到最后一张时,赋值为第一张
index = 1
boxWrap.style.left = -itemWidth*index+"px" // 这里的itemWidth为单张图片宽度
}
++index
move(boxWrap, itemWidth, 1000) // 调用运动函数
}

move = (el, target, t) => {
let s = parseInt(boxWrap.style.left)
let s1 = 0
let speed = target/t

let time2 = setInterval(()=&gt;{
    s1 += speed
    el.style.left = -s1 + s + &#34;px&#34;
    t--
    if (t === 0) {
        clearInterval(time2)
    }
}, 1)

}

这里发现向右的轮播正常了,但是发现时间明明设置的5秒钟的自动轮播,为什么不到5秒就执行了。这里当动画轮播启动时,需要终止自动轮播的计时器,结束以后再重新轮播,下面是关键代码。

代码语言:javascript
复制
run = (fn) => {
time = setInterval(()=>{
fn ? nextRun(fn) : nextRun()
},3000)
}

run(run)

move = (el, target, t, fn) => {
let s = parseInt(boxWrap.style.left)
let s1 = 0
let speed = target/t

clearInterval(time)

let time2 = setInterval(()=&gt;{
    s1 += speed
    el.style.left = -s1 + s + &#34;px&#34;
    t--
    if (t === 0) {
        if (fn) fn(fn)
        clearInterval(time2)
    }
}, 1)

}

nextRun = (fn) => {
if (index === itemLength - 1) {
index = 1
boxWrap.style.left = -itemWidth*index+"px"
}

++index

fn ?
move(boxWrap, itemWidth, 500, fn) :
move(boxWrap, itemWidth, 500)

}

完整代码

发现一个向右的自动轮播就完成了,向左同理改造move的第三个参数type为轮播方向,接下来的点击切换就简单了,下面是一个无缝轮播的完整代码。

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
padding: 0;
margin: 0;
}
#wrap{
position: relative;
width: 780px;
height: 400px;
margin: 50px auto;
border: 1px solid black;
overflow: hidden;
cursor: pointer;
}
#box-wrap{
position: absolute;
left: 0;
top: 0;
display: flex;
}
#box-wrap .item{
width: 780px;
height: 400px;
}
.item img{
width: 100%;
}
.red{
background-color: red;
}
.green{
background-color: green;
}
.black{
background-color: black;
}
.arrow{
position: absolute;
top: 50%;
width: 30px;
height: 30px;
transform: translate(0,-50%);
color: rgb(201, 200, 200);
font-size: 20px;
text-align: center;
line-height: 30px;
background: rgb(0, 0, 0, .3);
}
.arrow:hover{
background: rgb(0, 0, 0, 1);
color: white;
}
.btn{
user-select: none;
}
.btn .prev{
left: 0;
}
.btn .next{
right: 0;
}
</style>
</head>
<body>
<div id="wrap">
<div id="box-wrap">
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/147387/5/18994/136741/5fdc77afE5f82113e/f4c98e84e67f8fd0.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img11.360buyimg.com//babel/jfs/t1/138375/30/18878/225016/5fdcb77fE3ed18d79/71d924b7f529c6ea.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img12.360buyimg.com//babel/jfs/t1/144721/38/17890/108504/5fd37694E682d34fc/9d1ac8a5d13b94f8.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img10.360buyimg.com//babel/jfs/t1/148646/26/18903/93961/5fdc9321E51f8e513/c4324e7ea048805c.jpg!q80.webp" alt="">
</div>
<div class="item">
<img src="https://img14.360buyimg.com//babel/jfs/t1/155501/29/10296/70544/5fdc8f2fE6b2fab26/5423c671aa4e21bf.jpg!q80.webp" alt="">
</div>
</div>
<div class="btn">
<div class="arrow prev" id="left-arrow"><</div>
<div class="arrow next" id="right-arrow">></div>
</div>
</div>

&lt;script&gt;   
    window.onload = () =&gt; {
        (()=&gt;{
            let index = 1
            let wrap = document.querySelector(&#34;#wrap&#34;)
            let boxWrap = document.querySelector(&#34;#box-wrap&#34;)
            let imgLen = document.querySelectorAll(&#39;#box-wrap div&#39;).length
            let img_first = document.querySelectorAll(&#39;#box-wrap div&#39;)[0].cloneNode(true)
            let img_last = document.querySelectorAll(&#39;#box-wrap div&#39;)[imgLen - 1].cloneNode(true)
            let leftArrow = document.querySelector(&#34;#left-arrow&#34;)
            let rightArrow = document.querySelector(&#34;#right-arrow&#34;)
            let itemWidth = document.querySelectorAll(&#34;#box-wrap div&#34;)[0].offsetWidth

            boxWrap.appendChild(img_first)
            boxWrap.insertBefore(img_last, document.querySelectorAll(&#34;#box-wrap div&#34;)[0])

            let itemLength = document.querySelectorAll(&#34;#box-wrap div&#34;).length

            boxWrap.style.width = itemWidth * itemLength + &#34;px&#34;
            boxWrap.style.left = -itemWidth * index + &#34;px&#34;

            let time = null
            let isMove = true

            run = (fn) =&gt; {
                time = setInterval(()=&gt;{
                    fn ? nextRun(fn) : nextRun()
                },3000)
            }

            run(run)

            move = (el, target, type, t, fn) =&gt; {
                let s = parseInt(boxWrap.style.left)
                let s1 = 0
                let speed = target/t
                
                clearInterval(time)

                let time2 = setInterval(()=&gt;{
                    s1 += speed
                    if (type === &#34;left&#34;) {
                        el.style.left = -s1 + s + &#34;px&#34;
                    }else{
                        el.style.left = s1 + s + &#34;px&#34;
                    }
                    t--
                    if (t === 0) {
                        isMove = true
                        if (fn &amp;&amp; time) fn(fn)
                        clearInterval(time2)
                    }
                }, 1)
            }

            wrap.onmouseenter = () =&gt; {
                clearInterval(time)
                time = null
            }

            wrap.onmouseleave = () =&gt; {
                run(run)
            }

            nextRun = (fn) =&gt; {
                if (!isMove) return
                
                isMove = false
                
                if (index === itemLength - 1) {
                    index = 1
                    boxWrap.style.left = -itemWidth*index+&#34;px&#34;       
                }

                ++index

                fn ?
                move(boxWrap, itemWidth, &#34;left&#34;, 500, fn) :
                move(boxWrap, itemWidth, &#34;left&#34;, 500)

            }

            prevRun = () =&gt; {
                if (!isMove) return

                isMove = false
                
                if (index === 1) {
                    index = itemLength - 1
                    boxWrap.style.left = -itemWidth * index + &#34;px&#34;
                }
                --index
                move(boxWrap, itemWidth, &#34;right&#34;, 500)
            }

            leftArrow.onclick = (e) =&gt; {
                prevRun()
            }

            rightArrow.onclick = (e) =&gt; {
                nextRun()
            }
        })()
    }
&lt;/script&gt;

</body>
</html>