如果不使用 jQuery 等外部库或 Angular 等框架,就能拥有一个具有无限效果的图片库,那就太棒了。一个普通的 JS 和 CSS,所以每个人都可以使用。
怎么办?
在最近的一个项目中,我实现了一个图像轮播,重点关注效率和响应能力,我想分享我的设计和编码决策背后的基本原理。让我们来分解一下:
轮播的结构是包含所有轮播图像的
<div>
。值得注意的是,我对第一张和最后三张图像使用了“急切”加载。这是因为当轮播最初加载或用户导航到末尾时,这些图像可能位于视口中或视口附近。预加载可确保尽快加载这些图像,从而增强用户体验。
我使用 CSS 自定义属性(变量)来更轻松地维护和更好的可扩展性。响应式设计是使用媒体查询来实现的。低于 576 像素宽度时,轮播一次显示一张图像(100% 宽度),高于该宽度时,一次显示三张图像(33.33% 宽度)。较小屏幕上的导航按钮
display: none
通过提供更多空间来增强移动用户体验。
JavaScript 类
Carousel
处理所有轮播功能。它使用设置为第一个真实图像的当前索引进行初始化,并考虑循环效果的重复(假)图像。 slide
函数更新索引并使用 CSS 过渡来实现平滑移动。特别关注触摸导航,这是现代网络界面的必备功能,允许用户通过滑动来导航轮播。
此实现符合注重性能、响应能力和用户体验的现代 Web 开发标准。我希望这个细分可以帮助任何想要实现类似功能的人。
编码快乐!
.HTML
<div class="image-carousel">
<div class="carousel-viewport">
<!--/ You need to fake last three items (use eager loading!) /-->
<div class="carousel-image"><img src="https://i.ibb.co/Gv9jq4z/sci-fi-scene-ossenbrueck-4.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/dQmdmM3/sci-fi-scene-ossenbrueck-5.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/LprBxDc/sci-fi-scene-ossenbrueck-6.png" class="img-fluid" alt="..." loading="eager"></div>
<!--/ All images/-->
<div class="carousel-image"><img src="https://i.ibb.co/Kr8N8D4/sci-fi-scene-ossenbrueck-1.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/pXvLw1D/sci-fi-scene-ossenbrueck-2.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/6Dg7bY1/sci-fi-scene-ossenbrueck-3.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/Gv9jq4z/sci-fi-scene-ossenbrueck-4.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/dQmdmM3/sci-fi-scene-ossenbrueck-5.png" class="img-fluid" alt="..." loading="lazy"></div>
<div class="carousel-image"><img src="https://i.ibb.co/LprBxDc/sci-fi-scene-ossenbrueck-6.png" class="img-fluid" alt="..." loading="lazy"></div>
<!--/ You need to fake first three items (use eager loading!) /-->
<div class="carousel-image"><img src="https://i.ibb.co/Kr8N8D4/sci-fi-scene-ossenbrueck-1.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/pXvLw1D/sci-fi-scene-ossenbrueck-2.png" class="img-fluid" alt="..." loading="eager"></div>
<div class="carousel-image"><img src="https://i.ibb.co/6Dg7bY1/sci-fi-scene-ossenbrueck-3.png" class="img-fluid" alt="..." loading="eager"></div>
</div>
<button class="carousel-control prev">Prev</button>
<button class="carousel-control next">Next</button>
</div>
.JS
document.addEventListener('DOMContentLoaded', () => {
class Carousel {
constructor(viewportSelector, prevSelector, nextSelector) {
this.viewport = document.querySelector(viewportSelector);
this.prevButton = document.querySelector(prevSelector);
this.nextButton = document.querySelector(nextSelector);
this.currentIndex = 3;
this.realImageCount = this.viewport.children.length / 2;
this.setupNavigation();
this.setupTouchNavigation();
this.renderViewport();
}
getImageWidth() {
return window.innerWidth <= 600 ? 100 : 33.33;
}
resetPositionFirst() {
this.viewport.classList.add('transition-disabled');
this.currentIndex = 3;
this.renderViewport();
this.viewport.offsetHeight; // Trigger reflow
this.viewport.classList.remove('transition-disabled');
this.slide(1);
}
resetPositionToEnd() {
this.viewport.classList.add('transition-disabled');
this.currentIndex = this.viewport.children.length - 6;
this.renderViewport();
this.viewport.offsetHeight; // Trigger reflow
this.viewport.classList.remove('transition-disabled');
this.slide(-1);
}
slide(direction) {
this.currentIndex += direction;
if (this.currentIndex > this.realImageCount + 4) {
this.resetPositionFirst();
} else if (this.currentIndex < 0) {
this.resetPositionToEnd();
} else {
this.renderViewport();
}
}
renderViewport() {
this.viewport.style.transform = `translateX(-${this.currentIndex * this.getImageWidth()}%)`;
}
setupNavigation() {
this.prevButton.addEventListener('click', () => this.slide(-1));
this.nextButton.addEventListener('click', () => this.slide(1));
}
setupTouchNavigation() {
let touchStartX = 0;
let touchEndX = 0;
this.viewport.addEventListener('touchstart', e => touchStartX = e.touches[0].clientX, {passive: true});
this.viewport.addEventListener('touchmove', e => touchEndX = e.touches[0].clientX, {passive: true});
this.viewport.addEventListener('touchend', () => {
if (touchStartX - touchEndX > 50) this.slide(1);
if (touchEndX - touchStartX > 50) this.slide(-1);
});
}
}
new Carousel('.carousel-viewport', '.prev', '.next');
});
.CSS
:root {
--carousel-control-bg: #fff;
--carousel-img-width: 33.33%;
--carousel-mobile-img-width: 100%;
}
.img-fluid {
width: 100%;
height: auto;
}
.image-carousel {
position: relative;
width: 100%;
overflow: hidden;
}
.carousel-viewport {
display: flex;
transition: transform 0.4s ease;
}
.carousel-viewport .carousel-image {
flex: 0 0 var(--carousel-mobile-img-width);
max-width: var(--carousel-mobile-img-width);
}
.carousel-control {
display: none;
position: absolute;
top: 50%;
background-color: #fff;
border: none;
cursor: pointer;
padding: 10px;
transform: translateY(-50%);
z-index: 2;
}
.carousel-control.prev {
left: 10px;
}
.carousel-control.next {
right: 10px;
}
.transition-disabled {
transition: none !important;
}
@media (min-width: 576px) {
.carousel-viewport .carousel-image {
flex: 0 0 var(--carousel-img-width);
max-width: var(--carousel-img-width);
}
.carousel-control {
display: block;
}
}