Html5 轮播图 - 从零手搓系列 纯原生 JavaScript CSS 实现
2023-08-02
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
最近发现用现成的前端图形框架,多多少少有点别扭
比如加载文件多速度慢,调整css会有少许冲突
百度搜索结果不出图,因为百度个ZZ要图片在img里src才能识别
等等
<br>
所以是想把线上项目的前端部分重构成几乎纯手搓的方案
最多能接受依赖vue,其余部分全用最简化的代码纯手搓
<br>
理想很美好但水平很有限,辣么多组件,搓到天荒地老也搓不完
真有这本事直接自己写个开源图形框架了到时候
路要一步步走,先从轮播图开始搓,本文简单讲述下过程和原理
<br>
PS:其实百度上也有很多前辈的手搓代码可以借鉴,不过我还是尽量自己来
<br>
先贴一下最终效果 大概用了100行代码实现的简易轮播图
<br>
**最终源码开源地址**
[https://github.com/zzzmhcn/carousel](https://github.com/zzzmhcn/carousel)
[https://gitee.com/zzzmhcn/carousel](https://gitee.com/zzzmhcn/carousel)
<br>
**最终效果在线演示**
[https://zzzmhcn.gitee.io/carousel/](https://zzzmhcn.gitee.io/carousel/)
<br>
**最终效果GIF图**
![carousel](https://cdn1.zzzmh.cn/v2/public/gif/carousel.webp)
<br>
## 折腾
原理是参考了CSDN的一位前辈的方案
先把图片横过来排一排
再用 `overflow: hidden` 隐藏超出范围的部分
横移的效果js控制用css3的 `transform: translateX(-100%)`
最后动画用css3的 `transition: transform .5s ease;`
顺便为了减少依赖,2个箭头用的iconfont阿里官方库里下的`svg`
实践一遍大概是这样
1. 先把图片横过来排一排 用flex实现
```html
<head>
...
<style>
.carousel {
display: flex;
}
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
}
</style>
</head>
<body>
<div class="carousel">
<img src="img/1.webp">
<img src="img/2.webp">
<img src="img/3.webp">
<img src="img/4.webp">
<img src="img/5.webp">
</div>
</body>
```
效果图
![](/api/file/getImage?fileId=64c9c20dda74050014005b5c)
<br>
2. 再把超出部分隐藏 这样就只能看到第一张图
(这里可以用max-width限制宽度,也可以以后在外层div根据显示器自适应宽度,需要注意图片宽度必须大于div的width否则会穿帮)
```html
<head>
...
<style>
.carousel {
max-width: 800px;
display: flex;
overflow: hidden;
}
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
}
</style>
</head>
<body>
...
</body>
```
效果图
![](/api/file/getImage?fileId=64c9c290da74050014005b5d)
<br>
3. 加2个按钮示意左右切换
(这里是特意用的js去添加dom方式实现,为了以后用轮播图只需要div包img,不需要任何多余代码)
```html
<head>
...
<style>
.carousel {
max-width: 800px;
display: flex;
overflow: hidden;
position: relative;
}
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
}
.carousel .control {
position: absolute;
width: 100%;
height: 100%;
}
.carousel .control .next, .carousel .control .prev {
position: absolute;
top: calc(50% - 2rem);
cursor: pointer;
user-select: none;
background: rgba(66, 66, 66, .66);
border-radius: 50%;
padding: 6px 8px 4px;
}
.carousel .control .next {
right: 1rem;
}
.carousel .control .prev {
left: 1rem;
}
</style>
</head>
<body>
<div class="carousel">
<img src="img/1.webp">
<img src="img/2.webp">
<img src="img/3.webp">
<img src="img/4.webp">
<img src="img/5.webp">
</div>
<script>
const init = function () {
const carousel = document.querySelector('.carousel');
const control = document.createElement('div');
control.className = 'control';
const prev = document.createElement('div');
prev.className = 'prev';
prev.innerHTML = '<svg t="1690880358537" viewBox="0 0 1024 1024" p-id="2638" width="24" height="24"><path style="fill: whitesmoke;" d="M659.748571 245.272381l-51.687619-51.687619-318.439619 318.585905 318.415238 318.268952 51.712-51.736381-266.703238-266.556952z" p-id="2639"></path></svg>';
const next = document.createElement('div');
next.className = 'next';
next.innerHTML = '<svg t="1690880498698" viewBox="0 0 1024 1024" p-id="2777" width="24" height="24"><path style="fill: whitesmoke;" d="M605.086476 512.146286L338.358857 245.272381l51.760762-51.687619 318.415238 318.585905L390.095238 830.415238l-51.687619-51.736381z" p-id="2778"></path></svg>';
control.appendChild(prev);
control.appendChild(next);
carousel.appendChild(control);
}
window.onload = init;
</script>
</body>
```
效果图
![](/api/file/getImage?fileId=64c9c44fda74050014005b64)
<br>
4. 最后给2个按钮写上切换方法
(到这个程度已经可以实现无动画瞬间切换了)
```html
<head>
...
<style>
.carousel {
max-width: 800px;
display: flex;
overflow: hidden;
position: relative;
}
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
}
.carousel .control {
position: absolute;
width: 100%;
height: 100%;
}
.carousel .control .next, .carousel .control .prev {
position: absolute;
top: calc(50% - 2rem);
cursor: pointer;
user-select: none;
background: rgba(66, 66, 66, .66);
border-radius: 50%;
padding: 6px 8px 4px;
}
.carousel .control .next {
right: 1rem;
}
.carousel .control .prev {
left: 1rem;
}
</style>
</head>
<body>
<div class="carousel">
<img src="img/1.webp">
<img src="img/2.webp">
<img src="img/3.webp">
<img src="img/4.webp">
<img src="img/5.webp">
</div>
<script>
const init = function () {
const carousel = document.querySelector('.carousel');
const size = carousel.querySelectorAll('img').length;
const prevImage = function () {
const index = Number(carousel.getAttribute('index')) < 1 ? size - 1 : Number(carousel.getAttribute('index')) - 1;
carousel.setAttribute('index', String(index));
document.querySelectorAll('.carousel img').forEach(img => {
img.style.transform = 'translateX(' + (index * -100) + '%)';
});
};
const nextImage = function () {
const index = Number(carousel.getAttribute('index')) >= size - 1 ? 0 : Number(carousel.getAttribute('index')) + 1;
carousel.setAttribute('index', String(index));
document.querySelectorAll('.carousel img').forEach(img => {
img.style.transform = 'translateX(' + (index * -100) + '%)';
})
};
const control = document.createElement('div');
control.className = 'control';
const prev = document.createElement('div');
prev.className = 'prev';
prev.innerHTML = '<svg t="1690880358537" viewBox="0 0 1024 1024" p-id="2638" width="24" height="24"><path style="fill: whitesmoke;" d="M659.748571 245.272381l-51.687619-51.687619-318.439619 318.585905 318.415238 318.268952 51.712-51.736381-266.703238-266.556952z" p-id="2639"></path></svg>';
const next = document.createElement('div');
next.className = 'next';
next.innerHTML = '<svg t="1690880498698" viewBox="0 0 1024 1024" p-id="2777" width="24" height="24"><path style="fill: whitesmoke;" d="M605.086476 512.146286L338.358857 245.272381l51.760762-51.687619 318.415238 318.585905L390.095238 830.415238l-51.687619-51.736381z" p-id="2778"></path></svg>';
prev.addEventListener('click', prevImage);
next.addEventListener('click', nextImage);
control.appendChild(prev);
control.appendChild(next);
carousel.appendChild(control);
}
window.onload = init;
</script>
</body>
</html>
```
效果图
![carousel-demo](https://cdn1.zzzmh.cn/v2/public/gif/carousel-demo.webp)
5. 最后给个动画效果 非常简单一句话
在img的css里加 `transition: transform .5s ease;`
```css
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
/* 切换动画 改变transform时触发 0.5秒内完成 */
transition: transform .5s ease;
}
```
<br><br>
**大功告成!!!**
<br>
**最终效果图**
![carousel](https://cdn1.zzzmh.cn/v2/public/gif/carousel.webp)
<br>
**2023年8月2日更新**
后续我又补上了2个功能
1. 定时自动播放
2. 锚点精确跳转
没时间解释了,你可以自己点击在线体验地址试一下效果
<br>
**在线体验**
[https://zzzmhcn.gitee.io/carousel/](https://zzzmhcn.gitee.io/carousel/)
<br>
**完整代码**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>轮播图demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
.carousel {
max-width: 800px;
display: flex;
position: relative;
overflow: hidden;
}
.carousel .control {
position: absolute;
width: 100%;
height: 100%;
}
.carousel .control .next, .carousel .control .prev {
position: absolute;
top: calc(50% - 2rem);
cursor: pointer;
user-select: none;
background: rgba(66, 66, 66, .66);
border-radius: 50%;
padding: 6px 8px 4px;
}
.carousel .control .next {
right: 1rem;
}
.carousel .control .prev {
left: 1rem;
}
.carousel img {
width: 100%;
display: flex;
justify-content: flex-start;
/* 切换动画 改变transform时触发 0.5秒内完成 */
transition: transform .5s ease;
}
.carousel .control .indicators {
position: absolute;
right: 0;
bottom: 2rem;
left: 0;
z-index: 2;
display: flex;
justify-content: center;
}
.carousel .control .indicators .indicator {
flex: 0 1 auto;
width: 30px;
height: 3px;
margin-right: 3px;
margin-left: 3px;
cursor: pointer;
background-color: #fff;
background-clip: padding-box;
opacity: .5;
border-top: 1rem solid transparent;
border-bottom: 1rem solid transparent;
transition: opacity .5s ease;
}
</style>
</head>
<body>
<!--
class="carousel" 为必填项
autoplay="true" 为选填项 开启自动播放
timeout="1000" 为选填项 开启自动播放间隔时间 默认5000
-->
<div class="carousel" autoplay="true" timeout="3000">
<img src="img/1.webp">
<img src="img/2.webp">
<img src="img/3.webp">
<img src="img/4.webp">
<img src="img/5.webp">
</div>
<script>
const init = function () {
const carousel = document.querySelector('.carousel');
const size = carousel.querySelectorAll('img').length;
const autoplay = carousel.getAttribute("autoplay") === "true";
const timeout = carousel.getAttribute("timeout") ? Number(carousel.getAttribute("timeout")) : 5000;
const auto = function () {
carousel.setAttribute('index', String(Number(carousel.getAttribute('index')) >= size - 1 ? 0 : Number(carousel.getAttribute('index')) + 1));
};
// 每5秒自动切换下一张
let interval = autoplay ? window.setInterval(auto, timeout) : null;
const observer = new MutationObserver(function (mutations) {
mutations.forEach(e => {
// 改为监听 attribute 的方案统一处理变化
if (e.target.className == 'carousel') {
const index = e.target.getAttribute('index');
document.querySelectorAll('.carousel img').forEach(img => {
img.style.transform = 'translateX(' + (index * -100) + '%)';
});
document.querySelectorAll('.indicator').forEach((indicator, i) => {
indicator.style.opacity = (i === Number(index) ? "1" : "0.5");
});
// 数值发生变化自动重新计算 避免闪屏
if(autoplay){
clearInterval(interval);
interval = window.setInterval(auto, timeout);
}
}
});
});
observer.observe(carousel, {attributes: true});
// 用js添加控制器模块dom 减少html中代码
const control = document.createElement('div');
control.className = 'control';
const prev = document.createElement('div');
prev.className = 'prev';
prev.innerHTML = '<svg t="1690880358537" viewBox="0 0 1024 1024" p-id="2638" width="24" height="24"><path style="fill: whitesmoke;" d="M659.748571 245.272381l-51.687619-51.687619-318.439619 318.585905 318.415238 318.268952 51.712-51.736381-266.703238-266.556952z" p-id="2639"></path></svg>';
const next = document.createElement('div');
next.className = 'next';
next.innerHTML = '<svg t="1690880498698" viewBox="0 0 1024 1024" p-id="2777" width="24" height="24"><path style="fill: whitesmoke;" d="M605.086476 512.146286L338.358857 245.272381l51.760762-51.687619 318.415238 318.585905L390.095238 830.415238l-51.687619-51.736381z" p-id="2778"></path></svg>';
prev.addEventListener('click', function () {
carousel.setAttribute('index', String(Number(carousel.getAttribute('index')) < 1 ? size - 1 : Number(carousel.getAttribute('index')) - 1));
});
next.addEventListener('click', function () {
carousel.setAttribute('index', String(Number(carousel.getAttribute('index')) >= size - 1 ? 0 : Number(carousel.getAttribute('index')) + 1));
});
const indicators = document.createElement('div');
indicators.className = 'indicators';
for (let i = 0; i < size; i++) {
const indicator = document.createElement('div');
indicator.className = 'indicator';
indicator.addEventListener('click', function () {
carousel.setAttribute('index', String(i));
});
indicators.appendChild(indicator);
}
control.appendChild(prev);
control.appendChild(next);
control.appendChild(indicators);
carousel.appendChild(control);
// 初始化index
carousel.setAttribute('index', '0');
}
window.onload = init;
</script>
</body>
</html>
```
<br>
## END
**源码开源地址**
[https://github.com/zzzmhcn/carousel](https://github.com/zzzmhcn/carousel)
[https://gitee.com/zzzmhcn/carousel](https://gitee.com/zzzmhcn/carousel)
<br>
目前完成度还是较低,只是日常够用,存在几个不足
以后有时间有机会再继续补足
1. ~~无操作时,自动播放功能,类似幻灯片~~
2. 图片懒加载,以及未加载图片自动占位(这条和百度必须图片在img的src里有可能冲突,暂时不做)
3. ~~图片只能左右切换,别人的轮播图有下面一排小黑点可以指定切换哪一张~~
<br>
**2023年8月2日更新**
(上述问题 1 和 3 已实现并更新到线上)
<br>
累了就先这样吧
![](/api/file/getImage?fileId=64c9c860da74050014005b69)