前言
轮播图(Carousel)是前端开发中最基础也最经典的组件之一。看似简单,但要实现丝滑、无缝的循环切换效果,其实隐藏着许多关于 DOM 操作和 CSS 动画的细节。本文将带你从基础版过渡到进阶无缝版。
一、 基础版:基于 Transform 的位移实现
1. 核心原理
- 外层容器:设置
overflow: hidden,宽度固定。 - 内层轨道:使用
display: flex让图片横向排列。 - 位移逻辑:通过修改轨道的
transform: translateX()值来实现切换。
2. 存在的问题
当你从最后一张点击“下一张”时,为了回到第一张,轨道会向相反方向快速滑过中间所有图片。这种“视觉回退”体验很差,无法达到真正的“循环”感。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>简单轮播图 - 方法1</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
width: 800px;
background: white;
border-radius: 10px;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
/* 轮播图容器 */
.carousel-container {
width: 100%;
height: 400px;
position: relative;
overflow: hidden;
border-radius: 8px;
}
/* 轮播图列表 */
.carousel-list {
display: flex;
height: 100%;
transition: transform 0.5s ease-in-out;
}
/* 轮播图项 */
.carousel-slide {
min-width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
color: white;
}
/* 左右导航按钮 */
.carousel-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
transition: background 0.3s;
}
.carousel-button:hover {
background: rgba(0, 0, 0, 0.8);
}
.button-prev {
left: 10px;
}
.button-next {
right: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>简单轮播图 - 方法1: transform实现</h1>
<div class="carousel-container" id="carousel-container">
<div class="carousel-list" id="carousel-list">
<!-- 图片项,用不同颜色表示不同图片 -->
<div class="carousel-slide" style="background: #ff6b6b">图片 1</div>
<div class="carousel-slide" style="background: #4ecdc4">图片 2</div>
<div class="carousel-slide" style="background: #45b7d1">图片 3</div>
<div class="carousel-slide" style="background: #96ceb4">图片 4</div>
<div class="carousel-slide" style="background: #ffeaa7">图片 5</div>
</div>
<!-- 导航按钮 -->
<button id="button-prev" class="carousel-button button-prev">‹</button>
<button id="button-next" class="carousel-button button-next">›</button>
</div>
</div>
<script>
const carouselContainer = document.getElementById('carousel-container') //轮播图容器
const carouselList = document.getElementById("carousel-list"); // 轮播列表
const slides = Array.from(carouselList.children); // 被轮播的图片
const prevButton = document.getElementById("button-prev"); //上一张按钮
const nextButton = document.getElementById("button-next"); //下一张按钮
let currentIndex = 0; // 当前轮播的图片索引
const slideWidth = slides[0].getBoundingClientRect().width; // 显示的图片宽度
// 移动到指定索引
function moveToSlide(index) {
// 处理边界
if (index < 0) index = slides.length - 1;
if (index >= slides.length) index = 0;
// 移动轨道
carouselList.style.transform = "translateX(-" + slideWidth * index + "px)";
// 更新当前索引
currentIndex = index;
}
// 下一张
function nextSlide() {
moveToSlide(currentIndex + 1);
}
// 上一张
function prevSlide() {
moveToSlide(currentIndex - 1);
}
// 事件监听
prevButton.addEventListener("click", prevSlide);
nextButton.addEventListener("click", nextSlide);
</script>
</body>
</html>
二、 进阶版:无缝循环轮播(Seamless Loop)
1. 核心改进思路:克隆节点
为了实现无限向后滚动的假象,我们需要在轨道的头部和尾部做“手脚”:
- 克隆第一张图片放到列表最后。
- 克隆最后一张图片放到列表最前。
2. 实现流程
-
初始偏移:由于开头插入了克隆节点,初始
currentIndex设为1。 -
正常滑动:点击时正常开启动画滑动。
-
瞬间瞬移(核心) :
- 监听轨道的
transitionend事件。 - 当动画结束,发现当前在“最后一张克隆图”时,立即关闭动画(transition: none) ,并瞬间跳回“真正的第一张”。
- 监听轨道的
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>无缝轮播图 - 改进版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
display: flex; justify-content: center; align-items: center;
height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container { width: 800px; background: white; border-radius: 10px; padding: 20px; }
h1 { text-align: center; color: #333; margin-bottom: 20px; }
.carousel-container {
width: 100%; height: 400px; position: relative;
overflow: hidden; border-radius: 8px;
}
.carousel-list {
display: flex; height: 100%;
/* 初始不加 transition,由 JS 控制开关 */
}
.carousel-slide {
min-width: 100%; height: 100%;
display: flex; justify-content: center; align-items: center;
font-size: 3rem; color: white;
}
.carousel-button {
position: absolute; top: 50%; transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5); color: white; border: none;
width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
font-size: 1.2rem; transition: background 0.3s; z-index: 10;
}
.button-prev { left: 10px; }
.button-next { right: 10px; }
</style>
</head>
<body>
<div class="container">
<h1>无缝轮播图 - 改进版</h1>
<div class="carousel-container">
<div class="carousel-list" id="carousel-list">
<div class="carousel-slide" style="background: #ff6b6b">图片 1</div>
<div class="carousel-slide" style="background: #4ecdc4">图片 2</div>
<div class="carousel-slide" style="background: #45b7d1">图片 3</div>
<div class="carousel-slide" style="background: #96ceb4">图片 4</div>
<div class="carousel-slide" style="background: #ffeaa7">图片 5</div>
</div>
<button id="button-prev" class="carousel-button button-prev">‹</button>
<button id="button-next" class="carousel-button button-next">›</button>
</div>
</div>
<script>
const carouselList = document.getElementById("carousel-list");
console.log(carouselList)
const slides = Array.from(carouselList.children);
const prevButton = document.getElementById("button-prev");
const nextButton = document.getElementById("button-next");
// 1. 克隆第一张和最后一张
const firstClone = slides[0].cloneNode(true);
const lastClone = slides[slides.length - 1].cloneNode(true);
// 2. 将克隆节点添加到列表
carouselList.appendChild(firstClone);
carouselList.insertBefore(lastClone, slides[0]);
// 3. 重新计算状态
let currentIndex = 1; // 因为前面插入了一个,所以初始索引为 1
const slideWidth = slides[0].getBoundingClientRect().width; // 使用百分比计算更稳健
console.log(slides[0].getBoundingClientRect().width)
// 初始化显示第一张(不是克隆的第0张)
console.log(`translateX(-${currentIndex * slideWidth})`)
carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
function moveToSlide(index, hasAnimation = true) {
currentIndex = index;
// 开启动画
carouselList.style.transition = hasAnimation ? "transform 0.5s ease-in-out" : "none";
carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
}
// 核心:监听过渡结束
carouselList.addEventListener('transitionend', () => {
// 如果到了最后的克隆图,瞬间跳回真正的第一张
if (currentIndex === carouselList.children.length - 1) {
carouselList.style.transition = "none";
currentIndex = 1;
carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
}
// 如果到了最前的克隆图,瞬间跳回真正的最后一张
if (currentIndex === 0) {
carouselList.style.transition = "none";
currentIndex = carouselList.children.length - 2;
carouselList.style.transform = `translateX(-${currentIndex * slideWidth}px)`;
}
});
nextButton.addEventListener("click", () => moveToSlide(currentIndex + 1));
prevButton.addEventListener("click", () => moveToSlide(currentIndex - 1));
</script>
</body>
</html>