原生javascript手写一个丝滑的轮播图
创始人
2024-06-02 16:51:57
0

通过本文,你将学到:

  • html
  • css
  • js

没错,就是html,css,js,现在是框架盛行的时代,所以很少会有人在意原生三件套,通过本文实现一个丝滑的轮播图,带你重温html,css和js基础知识。

为什么选用轮播图做示例?有如下几点:

  • 业务当中最常用
  • 轮播图说简单也不简单,说复杂也不复杂,可以说是一切项目的基石
  • 轮播图更适合考察你对html,css,js的基础掌握

废话不多说,让我们先来看一下效果图,如下:

通过上图,我们可以知道,一个轮播图包含了三大部分,第一部分是轮播图的部分,第二部分则是轮播翻页部分,第三部分则是上一页和下一页。

所以一个轮播图的结构我们基本上就清晰了,让我们来详细看一下吧。

html文档结构

首先我们要有一个容器元素,如下:


然后,我们第一部分轮播图也需要一个容器元素,随后就是轮播图的元素列表,结构如下:

 

分析下来就三个,容器元素,每一个轮播图元素里面再套一个图片元素。

接下来是第二部分,同样的也是一个容器元素,套每一个轮播点元素,如下:

0
1
2
3
4
5
6

无论是轮播图部分还是轮播分页部分,都加了一个active类名,作为默认显示和选中的轮播图和轮播分页按钮。

第三部分,则是上一页和下一页按钮,这里如果我们将最外层的轮播容器元素设置了定位,这里也就不需要一个容器元素了,我们直接用定位,下一节写样式会详细说明。我们还是来看结构,如下:

<
>

这里采用了html字符实体用作上一页和下一页文本,关于什么是html字符实体,可以参考相关文章,这里不做详解。

通过以上的分析,我们一个轮播图的文档结构就完成了,接下来,让我们编写样式。

编写样式

首先我们根据效果图可以知道,容器元素,轮播图部分容器元素以及每一个轮播图元素都是百分之百宽高的,样式如下:

.carousel-box,.carousel-content,.carousel-item ,.carousel-item-img {width: 100%;height: 100%;
} 

其次,容器元素和轮播图元素,我们需要设置成相对定位,为什么轮播图也要设置成相对定位?因为我们这里是使用的绝对定位加left和right偏移从而实现的滑动轮播效果。

.carousel-box,.carousel-item {position: relative;
} 

然后,由于轮播图只显示当前的轮播图,而超出的部分也就是溢出部分我们需要截断隐藏,因此为容器元素设置截断隐藏。

.carousel-box {overflow: hidden;
} 

接着,每一个轮播图元素默认都是隐藏的,只有加了active类名,才显示。

.carousel-item {display: none;
}
.carousel-item.active {display: block;left: 0;
} 

再然后分别是向左还是向右,这里我们是通过添加类名的方式来实现滑动,所以我们在这里额外为轮播元素增加了left和right类名,如下:

.carousel-item.active.left {left: -100%;
}
.carousel-item.active.right {left: 100%;
} 

有意思的点还在这里,就是每一个轮播图元素还额外的增加了next和prev类名,为什么要增加这两个类名?试想我们当前轮播图显示的时候,前面的是不是应该被隐藏,而后面的下一个应该是紧紧排在当前轮播图之后,然后做准备,而这两个类名的目的就是在这里,让效果看起来更加丝滑一些。

.carousel-item.next,
.carousel-item.prev {display: block;position: absolute;top: 0;
}
.carousel-item.next {left: 100%;
}
.carousel-item.prev {left: -100%;
}
.carousel-item.next.left,
.carousel-item.prev.right {left: 0%;
} 

最后补充一个轮播图片元素的样式,如下:

.carousel-item-img {object-fit: cover;
} 

到了这里,其实轮播图的核心思路已经出现了,就是利用的绝对定位加left偏移来实现,而在javascript逻辑中,我们只需要操作类名就可以了。

这样做的好处很显然,我们将动画的逻辑包装在css中,因此轮播的动画逻辑也比较好修改,修改css代码总比修改js代码简单吧?

到了这里,轮播部分的样式我们就已经完成了,接下来看分页按钮组的样式。

根据图片示例,分页按钮组元素是在底部的,其实分页按钮组也可以说是很常规的按钮布局,所以样式都是一些很基础的,也没有必要做太多的详解。

.carousel-sign {position: absolute;bottom: 10px;left: 50%;transform: translateX(-50%);padding: 5px 3px;border-radius: 6px;user-select: none;background: linear-gradient(135deg,#73a0e4 10%,#1467e4 90%);
}.carousel-sign-item {width: 22px;height: 20px;font-size: 14px;font-weight: 500;line-height: 20px;text-align: center;float: left;color:#f2f3f4;margin: auto 4px;cursor: pointer;border-radius: 4px;
}
.carousel-sign-item:hover {color:#fff;
}
.carousel-sign-item.active {color:#535455;background-color: #ebebeb;
} 

最后就是上一页下一页的样式,有意思的是上一页下一页默认是不应该显示的,鼠标悬浮到轮播图容器元素上,才会显示,所以这里也用到了定位。

.carousel-ctrl {position: absolute;top: 50%;transform: translateY(-50%);font-size: 30px;font-weight: 300;user-select: none;background: linear-gradient(135deg,#73a0e4 10%,#1467e4 90%);color: #fff;border-radius: 5px;cursor: pointer;transition: all .1s cubic-bezier(0.075, 0.82, 0.165, 1);text-align: center;padding: 1rem;
}
.carousel-ctrl.carousel-left-ctrl {left: -50px;
}
.carousel-ctrl.carousel-right-ctrl {right: -50px;
}
.carousel-box:hover .carousel-ctrl.carousel-left-ctrl {left: 10px;
}
.carousel-box:hover .carousel-ctrl.carousel-right-ctrl {right: 10px;
}
.carousel-ctrl:hover {background-color: rgba(0,0,0,.8);
} 

到了这里,样式的布局就完成了,接下来是javascript核心逻辑,让我们一起来看一下吧。

轮播图的核心逻辑

我们将轮播图封装在一个类当中,然后通过构造函数调用的方式来使用它,我们先来看使用方式,如下:

const options = {el: '.carousel-box',speed: 1000, // 轮播速度(ms)delay: 0, // 轮播延迟(ms)direction: 'left', // 图片滑动方向monitorKeyEvent: true, // 是否监听键盘事件monitorTouchEvent: true // 是否监听屏幕滑动事件
}
const carouselInstance = new Carousel(options);
carouselInstance.start(); 

通过使用方式,我们得到了什么?

  • 轮播图的配置对象* el: 容器元素* speed: 轮播速度* delay: 轮播执行延迟时间* direction: 滑动方向,主要有left和right两个值* monitorKeyEvent: 是否监听键盘事件,也就是说是否可以通过点击键盘上的左右来切换轮播图* monitorTouchEvent: 是否监听屏幕滑动事件,也就是说是否可以通过滑动屏幕来切换图片* Carousel是一个构造函数* carousel构造函数内部提供了一个start方法用来开始轮播,很显然这里是开始自动轮播根据以上的分析,让我们来一步步实现Carousel这个东西吧。

首先它是一个构造函数,支持传入配置对象,所以,我们定义一个类,并且这个类还有一个start方法也初始化,如下:

class Carousel {constructor(options){//核心代码}start(){//核心代码}
} 

在初始化的时候我们需要做什么?

首先我们要获取到轮播元素,还有上一页下一页按钮以及我们的分页按钮元素。如下:

class Carousel {constructor(options){// 容器元素this.container = $(options.el);// 轮播图元素this.carouselItems = this.container.querySelectorAll('.carousel-item');// 分页按钮元素组this.carouselSigns = $$(('.carousel-sign .carousel-sign-item'),this.container);// 上一页与下一页this.carouselCtrlL = $$(('.carousel-ctrl'),this.container)[0];this.carouselCtrlR = $$(('.carousel-ctrl'),this.container)[1];}start(){//核心代码}
} 

这里用到了和和和$方法,看起来和jQuery的获取DOM很像,用到了jQuery?那当然不是了,我们来看这2个方法的实现。

const $ = (v,el = document) => el.querySelector(v);
const $$ = (v,el = document) => el.querySelectorAll(v); 

也就是获取dom元素的简易封装啦。

  • document.querySelector API
  • document.querySelectorAll API

就是基于以上两个dom查询节点的方法封装的,让我们来看下一步,首先我们需要有一个确定当前轮播图的索引值,然后获取所有轮播图元素的长度,然后就是初始化配置对象。代码如下:

class Carousel {constructor(options){//省略了代码// 当前图片索引this.curIndex = 0;// 轮播盒内图片数量this.numItems = this.carouselItems.length;// 是否可以滑动this.status = true;// 轮播速度this.speed = options.speed || 600;// 等待延时this.delay = options.delay || 3000;// 轮播方向this.direction = options.direction || 'left';// 是否监听键盘事件this.monitorKeyEvent = options.monitorKeyEvent || false;// 是否监听屏幕滑动事件this.monitorTouchEvent = options.monitorTouchEvent || false;}//省略了代码
} 

初始化完成之后,接下来我们有两个逻辑还需要在初始化里面完成,第一个逻辑是添加动画过渡效果,也就是让动画看起来更丝滑一些,第二个就是添加事件逻辑。继续在构造函数中调用2个方法,代码如下:

class Carousel {constructor(options){//省略了代码// 添加了事件this.handleEvents();// 设置过渡效果this.setTransition();}//省略了代码
} 

我们先来看最简单的setTransition方法,其实这个方法很简单,就是通过在head标签内添加一个style标签,通过insertRule方法添加样式。代码如下:

setTransition() {const styleElement = document.createElement('style');document.head.appendChild(styleElement);const styleRule = `.carousel-item {transition: left ${this.speed}ms ease-in-out}`styleElement.sheet.insertRule(styleRule, 0);
} 

很显然这里是为每个轮播图元素添加过渡效果,接下来我们来看绑定事件方法内部,我们可以尝试思考一下,都有哪些事件呢?总结如下:

  • 上一页与下一页
  • 分页按钮组
  • 滑动事件
  • 键盘事件
  • 轮播盒子元素的鼠标悬浮与鼠标移出事件

根据以上分析,我们的handleEvents方法就很好实现了,如下:

handleEvents() {// 鼠标从轮播盒上移开时继续轮播this.container.addEventListener('mouseleave', this.start.bind(this));// 鼠标移动到轮播盒上暂停轮播this.container.addEventListener('mouseover', this.pause.bind(this));// 点击左侧控件向右滑动图片this.carouselCtrlL.addEventListener('click', this.clickCtrl.bind(this));// 点击右侧控件向左滑动图片this.carouselCtrlR.addEventListener('click', this.clickCtrl.bind(this));// 点击分页按钮组后滑动到对应的图片for (let i = 0; i < this.carouselSigns.length; i++) {this.carouselSigns[i].setAttribute('slide-to', i);this.carouselSigns[i].addEventListener('click', this.clickSign.bind(this));}// 监听键盘事件if (this.monitorKeyEvent) {document.addEventListener('keydown', this.keyDown.bind(this));}// 监听屏幕滑动事件if (this.monitorTouchEvent) {this.container.addEventListener('touchstart', this.touchScreen.bind(this));this.container.addEventListener('touchend', this.touchScreen.bind(this));}
} 

这里有意思的点在于bind方法更改this对象,使得this对象指向当前轮播实例元素,还有一点就是我们为每个分页按钮设置了一个slide-to的索引值,后续我们就可以根据这个索引值来切换轮播图。

接下来,让我们看看每一个事件对应的回调方法,首先是start方法,其实很容易就想到,start方法就是开始轮播,开始轮播也就是自动轮播,自动轮播我们需要用到定时器,因此我们的start函数就很好实现了,代码如下:

start() {const event = {srcElement: this.direction == 'left' ? this.carouselCtrlR : this.carouselCtrlL};const clickCtrl = this.clickCtrl.bind(this);// 每隔一段时间模拟点击控件this.interval = setInterval(clickCtrl, this.delay, event);
} 

这里有意思的点在于我们的自动轮播,是直接去模拟点击上一页下一页进行切换的,除此之外,这里根据方向将srcElement元素作为事件对象传递,也就是说后面我们会根据这个元素来做方向上的判断。

接下来我们来看暂停函数,很简单,就是清除定时器即可,如下:

// 暂停轮播
pause() {clearInterval(this.interval);
} 

接下来,让我们来看clickCtrl方法,思考一下,我们是如何修改当前轮播图的索引值的,正常情况下,比如说,我们是向左滑动,索引值实际上就是将当前索引值相加,然后再判断是否超出了轮播图元素组的长度,重置索引值。

但是这里有一个更为巧妙的实现方式,那就是取模,将当前索引值加1然后与轮播图元素组的长度取模,这样也就保证了我们的索引值始终不会超过轮播图元素组的长度。

如果是向右滑动,那么我们应该是加上轮播图元素组的长度 - 1再取模,也就是与向左方向相反,根据以上分析,我们的代码就好实现了,如下:

// 处理点击控件事件
clickCtrl(event) {if (!this.status) return;this.status = false;let fromIndex = this.curIndex, toIndex, direction;if (event.srcElement == this.carouselCtrlR) {toIndex = (this.curIndex + 1) % this.numItems;direction = 'left';} else {toIndex = (this.curIndex + this.numItems - 1) % this.numItems;direction = 'right';}this.slide(fromIndex, toIndex, direction);this.curIndex = toIndex;
} 

在这个方法里面还有一个slide方法,顾名思义就是轮播图的滑动方法,其实通篇下来,最核心的就是这个slide方法,这个方法主要做的逻辑就是根据索引值来切换class。代码如下:

slide(fromIndex, toIndex, direction) {let fromClass, toClass;if (direction == 'left') {this.carouselItems[toIndex].className = "carousel-item next";fromClass = 'carousel-item active left';toClass = 'carousel-item next left';} else {this.carouselItems[toIndex].className = "carousel-item prev";fromClass = 'carousel-item active right';toClass = 'carousel-item prev right';}this.carouselSigns[fromIndex].className = "carousel-sign-item";this.carouselSigns[toIndex].className = "carousel-sign-item active";setTimeout((() => {this.carouselItems[fromIndex].className = fromClass;this.carouselItems[toIndex].className = toClass;}).bind(this), 50);setTimeout((() => {this.carouselItems[fromIndex].className = 'carousel-item';this.carouselItems[toIndex].className = 'carousel-item active';this.status = true;// 设置为可以滑动}).bind(this), this.speed + 50);
} 

这里分成了两块替换类名的逻辑,第一块是轮播图元素的替换类名,需要判断方向,第二块则是分页按钮组的类名替换逻辑,当然分页按钮组的逻辑是不需要替换类名的。

其实分析到这里,这个轮播图的核心基本已经完成了,接下来就是完善了,让我们继续看分页按钮组的事件回调逻辑。

其实这里的逻辑也就是拿到索引值,前面为每个分页按钮组设置了一个slide-to属性,这里很显然就通过获取这个属性,然后生成slide方法所需要的参数,最后调用slide方法就行了。代码如下:

clickSign(event) {if (!this.status) return;this.status = false;const fromIndex = this.curIndex;const toIndex = parseInt(event.srcElement.getAttribute('slide-to'));const direction = fromIndex < toIndex ? 'left' : 'right';this.slide(fromIndex, toIndex, direction);this.curIndex = toIndex;
} 

最后还剩两个逻辑,一个是键盘事件,另一个是滑动事件逻辑,我们先来看键盘事件逻辑,键盘事件逻辑无非就是利用keyCode判断用户是否点击的是上一页和下一页,然后像自动开始轮播那样模拟调用上一页下一页按钮事件逻辑即可。代码如下:

keyDown(event) {if (event && event.keyCode == 37) {this.carouselCtrlL.click();} else if (event && event.keyCode == 39) {this.carouselCtrlR.click();}
} 

最后就是滑动事件了,滑动事件最难的点在于如何判断用户滑动的方向,其实我们可以通过计算滑动角度来判断,这里的计算逻辑是来自网上的一个公式,感兴趣的可以查阅相关资料了解。我们来看代码如下:

touchScreen(event) {if (event.type == 'touchstart') {this.startX = event.touches[0].pageX;this.startY = event.touches[0].pageY;} else {// touchendthis.endX = event.changedTouches[0].pageX;this.endY = event.changedTouches[0].pageY;// 计算滑动方向的角度const dx = this.endX - this.startXconst dy = this.startY - this.endY;const angle = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);// 滑动距离太短if (Math.abs(dx) < 10 || Math.abs(dy) < 10) return;if (angle >= 0 && angle <= 45) {// 向右侧滑动屏幕,模拟点击左控件this.carouselCtrlL.click();} else if (angle >= 135 && angle <= 180) {// 向左侧滑动屏幕,模拟点击右控件this.carouselCtrlR.click();}}
} 

到此为止,我们的一个轮播图就完成了,总结一下我们学到了什么?

  • CSS过渡效果
  • CSS基本布局
  • javascript事件* 鼠标悬浮与移出事件* 滑动事件* 键盘事件* 元素点击事件
  • javascript定时器

PS: 以后再也不用自己写轮播了,业务当中遇到,可以直接拿去用了,😄。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

相关内容

热门资讯

少先队建队日主持词 少先队建队日主持词  什么是主持词  由主持人于节目进行过程中串联节目的串联词。如今的各种演出活动和...
晚会节目串词主持稿 晚会节目串词主持稿  在现在社会,很多地方都会使用到主持稿,通过主持稿的写作将主题贯穿于所有的节目之...
幼儿园开园揭牌剪彩仪式主持词 幼儿园开园揭牌剪彩仪式主持词  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在一步步...
公司辞旧迎新晚会主持词串词   男:尊敬的各位领导、各位来宾,  女:亲爱的同事们  合:大家下午好!  男:光阴似箭,岁月如梭...
纯中式婚礼主持词 纯中式婚礼主持词(通用5篇)  主持词是主持人在台上表演的灵魂之所在。在现在的社会生活中,越来越多的...
悟空传的经典台词 悟空传的经典台词  1、我曾深爱过,我不在乎结局。  2、我知道天会愤怒,那,你知不知道,天也会颤抖...
最有创意的广告词(经典 最有创意的广告词(经典  01 钱不是问题,问题是没钱。  02 钻石恆久远,一颗就破產。  03 ...
毕业感谢致辞 关于毕业感谢致辞(精选15篇)  无论是在学校还是在社会中,大家都写过致辞吧,致辞的措词造句要考虑与...
年会嘉宾简短致辞 年会嘉宾简短致辞  在日复一日的学习、工作或生活中,大家总少不了要接触或使用致辞吧,致辞具有很强的实...
成长礼主持稿 成长礼主持稿(通用8篇)  在日常生活和工作中,需要使用主持稿的情况越来越多,主持稿是在晚会、联欢会...
电视剧《放羊的星星》经典台词 电视剧《放羊的星星》经典台词  在现实社会中,用到台词的地方越来越多,台词是一种特殊的,也是很难掌握...
抓周仪式主持词 抓周仪式主持词范文  主持词是主持人在台上表演的灵魂之所在。在如今这个中国,主持词是活动、集会等的必...
年终总结大会主持词结束语 年终总结大会主持词结束语  主持词是各种演出活动和集会中主持人串联节目的串联词。时代不断在进步,主持...
纯中式婚礼主持词(2) 让我们共同举起手中的酒杯,共同祝福我们这一对知心爱人,祝福他们在爱的旅途上风雨相承,相濡以沫,真爱一...
幼儿园园庆主持词 幼儿园园庆主持词  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。在人们积极参与各种活动...
篮球比赛开幕式主持词 篮球比赛开幕式主持词(通用5篇)  主持词可以采用和历史文化有关的表述方法去写作以提升活动的文化内涵...
六一儿童节活动节目的主持词 六一儿童节活动节目的主持词(精选7篇)  主持词是各种演出活动和集会中主持人串联节目的串联词。在当今...
公司员工的感谢词 公司员工的感谢词3篇  我们虽然是公司的一名员工,其实也是公司的主人,需要有将公司当成家的态度,态度...
毕业晚会的主持稿 毕业晚会的主持稿(精选11篇)  在现在社会,我们很多时候都不得不用到主持稿,主持稿是主持人为节目进...
《加油金三顺》经典台词 《加油金三顺》经典台词  1、回忆是没有任何力量的。(三顺)  2、人都知道会死,但不还是活着吗?(...