轮播图使用的场景通常在网页首页上,在有限的空间可以通过轮播图,循环播放同一类型的图片、文字等内容。轮播图目前表现形式有 2 种,一种是常规的只出现一张图片,另一种是出现三张图片凸显一张的卡片化的。因为轮播图广泛使用,目前很多工具库(例如 swiper )都提供了现成的轮播图解决方案。但是作为一名合格的前端工程师不能只会使用现成的 api ,下面我来教你从零开始,从原理到代码一步步完成轮播图的制作。
知识点:
轮播图
事件防抖
通过上一篇博文,了解完 Ajax 和 Promise 的使用方法。接下来完成前端的接口开发。在 js 文件夹中新建 service 文件,同时在 service 文件夹下新建 ajax.js 文件(单独将前端请求接口提取到一个文件夹内,方便复用及调整)。在新建的文件中写入如下代码:
// ajax.js
const BASE_URL = "http://localhost:3000";
export default function Ajax({
//请求参数配置
method = "GET", //默认为'get'请求
url,
data = {},
}) {
return new Promise((resolve) => {
// 通过 Promise 返回异步请求
const xhr = new XMLHttpRequest();
xhr.open(method, BASE_URL + url);
xhr.onload = function () {
resolve(JSON.parse(xhr.response));
};
xhr.onerror = function () {
// 待最后进行错误处理操作
if (xhr.status == 0) {
}
};
xhr.send(JSON.stringify(data));
});
}
/**
* @description: 获得轮播图信息
* @param {*}
* @return {*}
*/
export async function getBannerList() {
const result = Ajax({
url: `/homepage/block/page`,
});
return result;
}
通过 ESM 在 home.js 文件中引入 Ajax,完成前端数据接收。
//home.js
import { getBannerList } from "../service/ajax.js";
const result = await getBannerList();
const carouselData = result.data.blocks[0].extInfo.banners;
轮播图初始化
在进行轮播图样式初始化前,我们首先要了解下轮播图的原理。轮播图其实是将所有的图片排成一排(列),在显示区域仅显示一张,指定时间后显示区域出现下一张图片,同一图片每隔一段时间循环出现。
为了项目更容易管理,我们在 home 文件夹内新建一个 carousel.js 的文件来单独完成轮播图
//carousel.js
// 切换箭头为静态 HTML 样式,无需根据图片数量动态生成。
const carouselControl = `
<button class="carousel-control carousel-control-left carousel-control-hover">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-arrow-left"></use>
</svg>
</button>
<button class="carousel-control carousel-control-right carousel-control-hover">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-arrow-right"></use>
</svg>
</button>
`;
//轮播图配置
const carousel = {
data: [], //轮播图数据
currentIndex: 0, //轮播图当前切换的画面
times: 2000, //轮播图多少时间切换画面
animationTimes: 0.5, //轮播图动画持续时间,单位s
autoCycleTimer: new Set(), //如果在切换动画,无法进行切换画面
};
export function carouselRender(data) {
//初始化轮播图
let carouselItem = "",
carouselIndicatorsLi = "";
const wrapper = document.querySelector(".carousel-wrapper");
let { width = 0 } = wrapper.getBoundingClientRect(); //得到图片的宽度
//动态生成轮播图
data.forEach((item, index) => {
//指示器激活选中判断
let isActive = carousel.currentIndex == index ? "active" : "";
//动态生成轮播图图片,并给每一张图片加上偏移量和动画效果
carouselItem += `
<div class="carousel-item ${
"#" + index
}" style='transform:translateX(${
width * (index - 1)
}px);transition-duration:${carousel.animationTimes}s'>
<img src="${item.pic}" alt="">
</div>
`;
//动态生成轮播图指示器
carouselIndicatorsLi += `
<li data-slide-to="${index}" class="carousel-indicators-li ${isActive}"></li>
`;
});
// 通过模板字符串,按照 home.html 中的 html 结构进行排布
const carouselContainer = `
<div class="carousel-container" style="transition:transform ${carousel.animationTimes}s ">
${carouselControl}
<div class="carousel-content">
${carouselItem}
</div>
</div>
`;
const carouselIndicators = `
<ul class="carousel-indicators d-flex">
${carouselIndicatorsLi}
</ul>
`;
// 将得到的字符串通过 innerHTML 插入到轮播图盒子
wrapper.innerHTML = carouselContainer + carouselIndicators;
}
Ajax({
url: "/homepage/block/page",
}).then((res) => {
console.log(res);
carousel.data = res.data.blocks[0].extInfo.banners;
//首次渲染轮播图
carouselRender(carousel.data);
});
注意在 home.css 文件中需要新增如下样式
/* home.css */
.carousel-container {
position: relative;
height: 300px;
width: 100%;
/* 新加的属性,调试过程中可先不加 */
overflow: hidden;
}
/* 新加的属性 */
.carousel-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.carousel-control {
width: 25px;
height: 25px;
line-height: 25px;
border-radius: 50%;
border: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2021;
background-color: rgba(255, 255, 255, 0.2);
text-align: center;
color: rgba(255, 255, 255, 0.6);
/* 新增 */
cursor: pointer;
}
.carousel-indicators > li {
width: 50px;
height: 4px;
background-color: rgba(255, 255, 255, 0.4);
margin: 15px 2px;
/* 新增 */
cursor: pointer;
}
1.自动轮播目前可以有以下两种方式,方案一:是在左右两侧均添加结尾和开始的图片,整体移动
该方法弊端在于需要新增图片,且在首尾切换的时候,会存在双倍的停留时间
方案二:每一张图片均进行移动,示意图如下:
此实验基于方案二来实现的,具体思路是通过改变数组的排序,数组的排序由当前显示区域图片的序号决定,开启定时器每隔一段事件序号+1 或者-1,既然知道思路了,下面我们就开始吧。
//carousel.js
function carouselRender(data) {
//初始化轮播图,代码同轮播图初始化
...
wrapper.innerHTML = carouselContainer + carouselIndicators;
// 通过定时器开启自动轮播,每过一段时间调用 getNext 方法
let timer = setInterval(getNext, carousel.times);
carousel.autoCycleTimer.add(timer);
}
function getPrev() {
// 获取到轮播图每一项的图片容器
const carouselItems = document.getElementsByClassName('carousel-item');
let length = carouselItems.length;
// 当后退到第一张时,重置为总长度,防止index变为负数导致bug
carousel.currentIndex == 0 && (carousel.currentIndex = length);
// 每调用一次 getPrev,序号-1
let index = carousel.currentIndex = --carousel.currentIndex % length;
// 将类数组转变为数组
let newArr = Array.from(carouselItems);
// 计算得到轮播图每一项的图片容器的宽度
let { width = 0 } = getElementRect(carouselItems[0]);
// 轮播图数组移动
newArr = [...newArr.slice(index), ...newArr.slice(0, index)];
newArr.forEach((item, i) => {// 轮播图数组第一项移动到最后一项,其他项顺序不变
if (i == 0) {
item.style.transform = `translateX(${width * (length - 1)}px)`;
item.style.opacity = 0;
}
item.style.transform = `translateX(${width * (i - 1)}px)`;
item.style.opacity = 1;
});
// 指示器移动
indicatorsRender(index);
}
function getNext() {
const carouselItems = document.getElementsByClassName('carousel-item');
let length = carouselItems.length;
let index = carousel.currentIndex = ++carousel.currentIndex % length;
let newArr = Array.from(carouselItems);
let lens = newArr.length;
let { width = 0 } = getElementRect(carouselItems[0]);
//当index为0时轮播图数组不做处理,>0时进行数组每一项移动
index != 0 && (newArr = [...newArr.slice(-index, lens), ...newArr.slice(0, lens - index)]);
newArr.forEach((item, i) => {
if (i == 0) {// 因为向右移动,轮播图数组最后一项移动到第一项,其他项顺序不变
item.style.transform = `translateX(${-width * (length - 1)}px)`;
item.style.opacity = 0;
}
item.style.transform = `translateX(${width * (i - 1)}px)`;
item.style.opacity = 1;
});
indicatorsRender(index)
}
function indicatorsRender(index) {
// 获取到轮播图每一项的指示器
const indicators = document.getElementsByClassName('carousel-indicators-li');
Array.from(indicators).forEach((item, i) => {
if (index == i) { // 当 index 和指示器下标相同添加active类
item.setAttribute('class', 'carousel-indicators-li active')
} else {
item.setAttribute('class', 'carousel-indicators-li')
}
})
}
function getElementRect(ele) {
return ele.getBoundingClientRect();
}
Ajax({
url: '/homepage/block/page'
}).then(res => {
console.log(res);
carousel.data = res.data.blocks[0].extInfo.banners;;
//首次渲染轮播图
carouselRender(carousel.data);
});
防抖
防抖就是特定时间内,防止重复操作执行多次事件处理函数。
防抖可以有以下应用场景:
登录、发请求等按钮避免用户点击太快,以致于发送了多次请求,需要防抖 — 立即执行
调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖 — 非立即执行
输入框内容校验时,等用户输入完成后再校验,需要用到防抖 — 非立即执行
因为防抖函数为公用函数,我们可以将其提出单独新建一个 js 文件,方便代码复用。 现在在 js 文件夹中新建 util 文件夹,新建 util.js 文件,导出 debounce 函数
// util.js
export function debounce(fn, times, isImmediately = true) {
//防抖函数
let timer = null;
let cb;
if (isImmediately) {
cb = function (...args) {
timer && clearTimeout(timer);
let isDone = !timer;
timer = setTimeout(() => {
timer = null;
}, times);
isDone && fn.apply(this, args);
};
} else {
cb = function (...args) {
const content = this;
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(content, args);
}, times);
};
}
return cb;
}
轮播图事件绑定
完成轮播图自动播放功能后,接下来还有三个功能需要添加:切换箭头实现图片上一张或者下一张切换;点击指示器跳转到指定图片;移入轮播图暂停轮播,移出轮播图重新轮播。
这三个功能都是通过事件来绑定的,我们可以将所有的事件处理放在同一个函数中,当轮播图初始化完成后集中触发
//carousel.js
import { debounce } from "../util/util.js";
function leftHandle() {
//左切换箭头事件处理
//清空定时器暂停轮播
clearAllTimer();
//切换到前一张
getPrev();
//开启定时器继续轮播,并将定时器加入到定时器保存器中
let timer = setInterval(getNext, carousel.times);
carousel.autoCycleTimer.add(timer);
}
function rightHandle() {
//右切换箭头事件处理
clearAllTimer();
getNext();
let timer = setInterval(getNext, carousel.times);
carousel.autoCycleTimer.add(timer);
}
//函数防抖
const leftHandleDebounce = debounce(leftHandle, 500);
const rightHandleDebounce = debounce(rightHandle, 500);
export function initCarouselEvent() {
const leftControl = document.getElementsByClassName("carousel-control-left");
const rightControl = document.getElementsByClassName(
"carousel-control-right"
);
const carouselContainer = document.querySelector(".carousel-container");
const indicatorsWrapper = document.querySelector(".carousel-indicators");
// 左右箭头切换事件
leftControl[0].addEventListener("click", leftHandleDebounce);
rightControl[0].addEventListener("click", rightHandleDebounce);
// 移入移出控制轮播播放事件
carouselContainer.addEventListener("mouseenter", () => {
//移入轮播图通过移除定时器达到轮播图暂停的目的
clearAllTimer();
});
carouselContainer.addEventListener("mouseleave", () => {
//移出轮播图通过设置定时器达到开启轮播图轮播的目的
let timer = setInterval(getNext, carousel.times);
carousel.autoCycleTimer.add(timer);
});
//指示器事件处理函数:通过事件委托到父级容器 ul,减少对每个指示器添加事件监听
indicatorsWrapper.addEventListener(
"mouseenter",
(e) => {
if (e.target.tagName === "LI") {
clearAllTimer();
// 得到每个指示器的序号
const index = e.target.getAttribute("data-slide-to");
// 序号-1,调用getNext会+1,两者相抵消,根据序号指定到对应的图片
carousel.currentIndex = index - 1;
getNext();
let timer = setInterval(getNext, carousel.times);
carousel.autoCycleTimer.add(timer);
}
},
true
);
}
function clearAllTimer() {
for (const i of carousel.autoCycleTimer) {
clearInterval(i);
if (carousel.autoCycleTimer > 100) {
carousel.autoCycleTimer.clear();
}
}
}
轮播图作为一个首页常用功能,大部分的工具库都有现成的代码,所以在这块我们重要的是掌握原理和思想,要学会将一个项目拆分成很多小的功能点,一步步的实现。
目前项目大都是,前后端分离的。对于向后端数据请求,用的最多的是 axios,但 axios 是基于 Ajax 做的,对于 Ajax 我们还是得掌握和熟练使用。