持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
本篇文章会使用纯原生的方式实现一个轮播图,能够无缝轮播,还用到了装饰器模式让轮播图下标的更新逻辑与轮播图样式的切换逻辑分离,做到数据和视图分离,最终的效果如下
手撕轮播图其实一点也不难的,相信看完这篇文章你也能够手撕它!
1. 加载图片
使用一个全局变量存放图片的列表,这里我为了简便,直接从unsplash
上找了几张图片
const imgUrlList = [
'https://unsplash.com/photos/hWoJA1OPdkQ/download?ixid=MnwxMjA3fDB8MXxhbGx8ODN8fHx8fHwyfHwxNjU0NTg4ODk1&force=true',
'https://unsplash.com/photos/0dC0h6CjOMM/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjU0NTkwNjM2&force=true',
'https://unsplash.com/photos/iBmuPRBJj8M/download?ixid=MnwxMjA3fDB8MXxhbGx8MTY0fHx8fHx8Mnx8MTY1NDU4ODkwNg&force=true',
'https://unsplash.com/photos/rZMiCdPAlss/download?ixid=MnwxMjA3fDB8MXxhbGx8MTg0fHx8fHx8Mnx8MTY1NDU4ODkxMg&force=true',
'https://unsplash.com/photos/faKvebx79FA/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTh8fG5lb258ZW58MHwwfHx8MTY1NDU5MDcwMw&force=true',
];
复制代码
unsplash 是一个图片资源网站,里面有大量高质量清晰的图片可以免费下载 unsplash.com/
然后实现一个loadImage
函数,调用它即可将图片动态添加到容器DOM
中
/**
* @description 加载图片到 container 中
* @param {HTMLElement} containerEl 图片容器
* @param {string[]} imgUrlList 图片 url 列表
*/
const loadImage = (containerEl, imgUrlList) => {
const render = () => {
/**
* @description 生成图片 html 模板
* @param {string} imgUrl 图片 url
*/
const genTemplate = (imgUrl) => {
return `
<img src="${imgUrl}">
`;
};
const html = [];
imgUrlList.forEach((imgUrl) => {
html.push(genTemplate(imgUrl));
});
containerEl.innerHTML = html.join('');
};
render();
};
复制代码
然后在初始化逻辑中调用loadImage
函数,传入容器DOM
元素和图片列表即可
const oCarousel = document.querySelector('.carousel');
/**
* @type {HTMLElement}
*/
const oCarouselContainer = oCarousel.querySelector('.container');
const init = () => {
// 初始化加载图片
loadImage(oCarouselContainer, imgUrlList);
};
复制代码
2. 图片切换实现思路
2.1 css left 样式实现图片偏移
有两层div
元素,外层控制轮播图总宽度,内层通过left
属性控制css
样式的偏移,要切换到第几张图片,则left
设置为轮播图总宽度 * 图片下标 * -1
乘以-1
是因为轮播图要往左滑,所以left
需要为负值才能让其往左边偏移 html
和css
代码如下
<div class="carousel">
<div class="container"></div>
</div>
复制代码
.carousel {
width: 600px;
height: 300px;
border: 4px solid darkgreen;
}
.carousel .container {
display: flex;
position: relative;
left: 0;
width: 100%;
height: 100%;
}
.carousel .container img {
/* shrink 设置为 0 防止图片缩小 保证图片宽度撑满整个容器 */
flex-shrink: 0;
height: 100%;
width: 100%;
/* 防止图片宽高比改变 */
object-fit: cover;
}
复制代码
下标为0
时,显示的效果如下 将下标改为1
,则会切换到第二张图片(最左边的容器是轮播图)
2.2 js 修改 left 样式值进行切换
既然已经知道了如何切换图片,那么接下来就只需要用js
获取轮播图的宽度,然后根据轮播图中当前展示的图片下标去修改container
的left
样式即可
const oCarousel = document.querySelector('.carousel');
/**
* @type {HTMLElement}
*/
const oCarouselContainer = oCarousel.querySelector('.container');
const carouselWidth = oCarousel.clientWidth;
let curIndex = 0;
/**
* @description 刷新轮播图的图片
* @param {number} index 轮播图图片的下标
*/
const refreshCarousel = () => {
oCarouselContainer.style.left = `${curIndex * carouselWidth * -1}px`;
};
复制代码
这样一来每次修改了curIndex
之后调用一下refreshCarousel
函数即可让轮播图切换
2.3 实现左右切换
添加两个左右切换的按钮,并为它们绑定点击事件,每次点击就修改curIndex
然后调用refreshCarousel
函数进行刷新
<div class="carousel">
<div class="container"></div>
<div class="left-right-btn-group">
<button id="left-btn">left</button>
<button id="right-btn">right</button>
</div>
</div>
复制代码
const handleLeftBtnClick = () => {
// 循环切换
curIndex = curIndex - 1 < 0 ? imgUrlList.length - 1 : curIndex - 1;
refreshCarousel();
};
const handleRightBtnClick = () => {
// 循环切换
curIndex = curIndex + 1 >= imgUrlList.length ? 0 : curIndex + 1;
refreshCarousel();
};
复制代码
这里的refreshCarousel
调用会比较常用,之后通过下标选择器切换轮播图图片时也会用到,因此可以考虑用装饰器模式将refreshCarousel
的调用包装起来,像下面这样:
/**
* @description 通过装饰器模式自动处理刷新轮播图的逻辑
* @param {Function} func function
*/
const refreshCarouselWrapper = (func) => {
return function (...args) {
const res = func(...args);
refreshCarousel();
return res;
};
};
复制代码
然后使用的时候专心处理目标逻辑即可,不需要管轮播图的刷新逻辑
const handleLeftBtnClick = () => {
- curIndex = curIndex - 1 < 0 ? imgUrlList.length - 1 : curIndex - 1;
- refreshCarousel();
+ refreshCarouselWrapper(() => {
+ // 循环切换
+ curIndex = curIndex - 1 < 0 ? imgUrlList.length - 1 : curIndex - 1;
+ })();
};
const handleRightBtnClick = () => {
- curIndex = curIndex + 1 >= imgUrlList.length ? 0 : curIndex + 1;
- refreshCarousel();
+ refreshCarouselWrapper(() => {
+ // 循环切换
+ curIndex = curIndex + 1 >= imgUrlList.length ? 0 : curIndex + 1;
+ })();
};
复制代码
现在就可以进行左右切换轮播图了
3. 下标选择器
3.1 根据图片数量生成下标选择器
首先创建一个存放下标选择器按钮的容器元素
<div class="carousel">
<div class="container"></div>
<div class="left-right-btn-group">
<button id="left-btn">left</button>
<button id="right-btn">right</button>
</div>
+ <div class="index-selector-btn-group"></div>
</div>
复制代码
然后实现一个初始化下标选择器的函数,根据图片的数量创建对应个数的按钮并添加到容器中
/**
* @description 根据轮播图中图片的数量初始化下标选择器
* @param {HTMLElement} container 按钮容器元素
* @param {number} imgCount 图片数量
*/
const initCarouselIndexSelector = (container, imgCount) => {
const genTemplate = (index) => {
return `
<button data-img-index="${index}">${index + 1}</button>
`;
};
const render = () => {
const html = [];
// 生成模板
for (let i = 0; i < imgCount; i++) {
html.push(genTemplate(i));
}
// 渲染
container.innerHTML = html.join('');
};
render();
};
复制代码
模板中给每个按钮绑定了一个data-img-index
属性,用于之后进行点击切换轮播图时修改curIndex
使用 现在下标选择器就被渲染出来了
3.2 为下标选择器按钮绑定事件
点击相应的下标选择器按钮,应当跳转到相应的下标,那么怎么获取到下标呢?前面在模板中已经给按钮绑定上了对应的图片下标数据了,可以通过DOM
元素的dataset
属性获取,像下面这样
const all = oIndexSelectorBtnGroup.querySelectorAll('button');
all.forEach((item) => {
console.log(item.dataset.imgIndex);
});
复制代码
那么只需要在事件绑定函数中给每个按钮绑定事件即可
const handleIndexSelectorBtnClick = (index) => {
refreshCarouselWrapper(() => {
curIndex = index;
})();
};
const bindEvent = () => {
// 左右切换按钮点击事件
oLeftBtn.addEventListener('click', handleLeftBtnClick);
oRightBtn.addEventListener('click', handleRightBtnClick);
// 下标选择器点击事件
oIndexSelectorBtnGroup.querySelectorAll('button').forEach((btn) => {
const index = btn.dataset.imgIndex;
btn.addEventListener(
'click',
handleIndexSelectorBtnClick.bind(null, index)
);
});
};
复制代码
这里由于点击事件需要接收index
参数,可以巧妙地通过bind
将参数绑定进去 现在就可以进行切换了 咦?这里明显有一个问题,点击切换到第二张图片之后,再次点击right
应当切换到第三张图片,但是居然又切换回第一张图片了,这是怎么回事?
3.3 修复下标选择器切换 bug
可以在右切换按钮点击事件中输出一下切换前后的curIndex
const handleRightBtnClick = () => {
refreshCarouselWrapper(() => {
console.log(`切换前 -- ${curIndex}`);
console.log(curIndex + 1 >= imgUrlList.length);
// 循环切换
curIndex = curIndex + 1 >= imgUrlList.length ? 0 : curIndex + 1;
console.log(`切换后 -- ${curIndex}`);
})();
};
复制代码
当我们点击第三张图片,然后点击right
按钮时,控制台输出如下: curIndex + 1 >= imgUrlList.length
这个判断居然是正确的?! 明明目前的curIndex === 2
,还没有超过图片总数,为什么会判断成true
呢?
再看看下面这个情况,先点击第一个按钮,切换到第一张图片,然后再点击right
按钮切换到下一张图片,控制台输出如下 就离谱!居然连01
这样的输出都出来了,其实这是因为当点击下标选择器后,curIndex
被修改成点击事件传入的参数,该参数来自button
元素的dataset
,而dataset
中取出的数据都是string
类型的!这个可以从MDN
中查到 那么我们将它转成number
类型再传给点击事件的处理函数即可
// 下标选择器点击事件
oIndexSelectorBtnGroup.querySelectorAll('button').forEach((btn) => {
- const index = btn.dataset.imgIndex;
+ const index = parseInt(btn.dataset.imgIndex);
btn.addEventListener(
'click',
handleIndexSelectorBtnClick.bind(null, index)
);
});
复制代码
现在就不会有这个奇怪的bug
了
4. 为轮播图添加切换动画
切换动画可以通过transition
实现,在添加该属性之前,先要思考一个问题 哪个元素才需要添加**transition**
属性? 谁要动就给谁添加!这里由于我们是通过修改container
容器的left
样式实现切换的,所以我们应该给container
元素加上transition
属性
.carousel .container {
display: flex;
position: relative;
left: 0;
width: 100%;
height: 100%;
+ transition: left 0.5s ease-in-out;
}
复制代码
5. 添加样式让轮播图更像轮播图
5.1 左右切换按钮样式
给左右按钮加上类名
<div class="carousel">
<div class="container"></div>
<div class="left-right-btn-group">
<button class="btn left" id="left-btn"><</button>
<button class="btn right" id="right-btn">></button>
</div>
<div class="index-selector-btn-group"></div>
</div>
复制代码
首先我们需要让左右切换按钮垂直居中,通过绝对定位,相对于.carousel
定位
- 让它的
top
和bottom
为0
- 上下外边距设置为
auto
(这个很关键)
其中top
和bottom
可以用inset
进行简写
.carousel .left-right-btn-group .btn {
position: absolute;
/* 让按钮垂直居中 */
inset: 0 auto;
width: 30px;
height: 30px;
/* 垂直居中的关键 -- 上下外边距自适应 */
margin: auto 0;
border-radius: 50%;
border: none;
cursor: pointer;
background-color: rgba(255, 255, 255, 0.3);
color: white;
}
复制代码
还要让左右按钮分别定位到左右两处
.carousel .left-right-btn-group .left {
left: 10px;
}
.carousel .left-right-btn-group .right {
right: 10px;
}
复制代码
还可以加上透明度的样式,当鼠标没有悬浮在左右切换按钮上时,让按钮半透明,悬浮时则让其完全显示
.carousel .left-right-btn-group .btn {
position: absolute;
/* 让按钮垂直居中 */
inset: 0 auto;
width: 30px;
height: 30px;
/* 垂直居中的关键 -- 上下外边距自适应 */
margin: auto 0;
border-radius: 50%;
border: none;
cursor: pointer;
background-color: rgba(255, 255, 255, 0.3);
color: white;
+ opacity: 0.5;
+ transition: opacity 0.5s ease;
}
+ .carousel .left-right-btn-group .btn:hover {
+ opacity: 1;
+ }
复制代码
5.2 下标选择器样式
首先将下标选择器按钮组水平居中在轮播图的底部
/* 下标选择器 */
.carousel .index-selector-btn-group {
position: absolute;
/* 绝对定位 水平居中的关键 */
left: 0;
right: 0;
bottom: 10px;
margin: 0 auto;
/* 让按钮组宽度和轮播图宽度一样宽 */
width: max-content;
}
复制代码
这里有两个关键点,一个是绝对定位时如何让元素水平居中,另一个是如何让按钮组的宽度和轮播图一样宽 然后我们修改渲染函数中使用到的按钮的模板,将下标去除
const initCarouselIndexSelector = (container, imgCount) => {
const genTemplate = (index) => {
return `
- <button data-img-index="${index}">${index + 1}</button>
+ <button class="btn" data-img-index="${index}"></button>
`;
};
// ...
}
复制代码
现在轮播图就变成这样了 我们要让每个按钮之间的间距大一些,现在的间距是原生button
元素的自带样式
/* 下标选择器 */
.carousel .index-selector-btn-group {
+ display: flex;
+ gap: 10px;
position: absolute;
/* 绝对定位 水平居中的关键 */
left: 0;
right: 0;
bottom: 10px;
margin: 0 auto;
/* 让按钮组宽度和轮播图宽度一样宽 */
width: max-content;
}
复制代码
再为按钮加上样式
.carousel .index-selector-btn-group .btn {
width: 20px;
height: 1px;
border: none;
cursor: pointer;
background-color: gray;
}
复制代码
5.3 下标选择器高亮当前展示的轮播图
以element-plus
的carousel
为例,当前激活的轮播图对应的下标选择器应当高亮显示 实现思路很简单,实现一个activeIndexSelector
函数,每次切换轮播图的图片的时候就去调用它,为当前激活的选择器按钮添加一个active
样式,然后为这个样式编写单独的背景颜色即可 所以核心就是activeIndexSelector
这个函数要如何实现
const activeIndexSelector = () => {
oIndexSelectorBtnGroup.querySelectorAll('.btn').forEach((item) => {
const imgIndex = parseInt(item.dataset.imgIndex);
if (imgIndex === curIndex) {
// 激活了则添加 active 类名
item.classList.add('active');
} else {
// 没有激活则将 active 类名移除
item.classList.remove('active');
}
});
};
复制代码
直接遍历按钮组,取出其中的imgIndex
数据,将其和curIndex
进行对比,如果相同则添加active
样式,如果不同则移除active
样式即可 最后别忘了在合适的时候调用该函数
- 切换轮播图的时候
- 点击左右切换按钮时
- 点击底部下标选择器时
- 初始化轮播图的下标选择器时
切换轮播图时的调用就可以利用到我们之前用装饰器模式封装的refreshCarouselWrapper
函数,在里面直接调用即可,不需要去修改左右切换按钮和下标选择器按钮的点击事件处理函数
/**
* @description 通过装饰器模式自动处理刷新轮播图的逻辑
* @param {Function} func function
*/
const refreshCarouselWrapper = (func) => {
return function (...args) {
const res = func(...args);
refreshCarousel();
+ activeIndexSelector(); // 更新选择器的高亮
return res;
};
};
复制代码
这就是使用装饰器模式的好处,一定程度上让curIndex
的更新逻辑和视图的渲染逻辑分离,解除它们之间的耦合 还有要在初始化下标选择器的时候进行调用
/**
* @description 根据轮播图中图片的数量初始化下标选择器
* @param {HTMLElement} container 按钮容器元素
* @param {number} imgCount 图片数量
*/
const initCarouselIndexSelector = (container, imgCount) => {
const genTemplate = (index) => {
return `
<button class="btn" data-img-index="${index}"></button>
`;
};
const render = () => {
const html = [];
// 生成模板
for (let i = 0; i < imgCount; i++) {
html.push(genTemplate(i));
}
// 渲染
container.innerHTML = html.join('');
};
render();
+ // 渲染之后还要让当前激活的选择器高亮
+ activeIndexSelector();
};
复制代码
最后给.active
添加样式就大功告成了!
.carousel .index-selector-btn-group .active {
background-color: #fff;
}
复制代码
完整代码: