用原生js来写一个swiper滑块插件
是不是有点印象了,没错,他的最基本的用法就是左右滑动,插件使用者只需要写几行简单的html和js即可实现一个简单滑动效果,不过你完全可以组合各种元素来适应不同的场景。
当然插件我已经写好了,咱先看下这个插件是怎么来用的,对插件有一个大概了解,一会写起来不至于太懵逼。。。
插件地址:https://github.com/laravuel/swiper.git
demo目录有演示和用法,不过插件我用了webpack和babel转码,可以不用管,直接看src/swiper.js即可。
<!-- demo.html --> <!-- swiper名称可以自定义的啦 -->
<div id="swiper">
<!-- swiper-item名称也可以自定义啦,相当于一个滑块 -->
<div class="swiper-item">
<img src="./images/1.jpg" />
</div>
<div class="swiper-item">
<img src="./images/2.jpg" />
</div>
<div class="swiper-item">
<img src="./images/3.jpg" />
</div>
</div> <script src="../dist/swiper.js"></script>
<script>
new Swiper({
swiper: '#swiper', // swiper节点名称
item: '.swiper-item', // swiper内部滑块的节点名称
autoplay: false, // 是否自动滑动
duration: 3000, // 自动滑动间隔时间
change(index) { // 每滑动一个滑块,插件就会触发change函数,index表示当前的滑块下标
console.log(index);
}
});
</script>
就是这么简单,插件本身只是一个类,你只需要new一个对象出来,然后传递一些参数就ok了。而且,插件还提供了一个change方法,让使用者可以在外部控制滑块的滑动!
const swiper = new Swiper({...});
swiper.change(2); // 滑动到第三个滑块
那么接下来,就是我们的教程时间了,我也不确定你能不能硬着头皮看完,不过我敢肯定,如果你能够亲手把插件写出来,你肯定会开心的飞起!!!
由于本次教程内容比较多,所以我分上下两部分来讲,第一部分主要讲解原理,第二部分开始着手编写插件。所以,感兴趣的小伙伴可以加个关注先。
1. 功能分析
俗话说,一上来就贴代码纯属耍流氓~
我们要清楚自己想实现哪些功能,懒得思考的童鞋可以结合我上面的动图来分析:
- 滑块可以左右滑动(支持移动端和pc端)
- 滑块块内部可以写任何元素
- 滑动到第一个和最后一个滑块时会有一个限制,防止越界
- 能够自动播放
我们所能看到的大概就这些,接下来我们会对这些功能一一进行拆解和分析。
2. 实现原理
上面简单梳理了一些功能,其实可以再扩展出以下几个问题:
- 滑块的html结构是什么样的?
- 滑块的滑动原理是什么?
- 如何来触发滑动?
别急,一个个来
2.1.1 滑块的html结构是什么样的?
我们先来看一张图:
这就是一个滑块的最基本的结构图,有三个部分组成:
- 视图
我们的内容展示区域,相当于最外层的一个展示层- 容器
容器的宽度是无限长的,容纳我们所有需要切换的内容,滑块的左右滑动,实质上是容器的左右移动(left),而每个滑块相对于容器其实是静止的- 滑块
一个个的内容
那么根据这个结构,可以用如下html代码来表示:
<!-- 视图层 -->
<div class="swiper">
<!-- 容器 -->
<div class="swiper-container">
<!-- 滑块 -->
<div class="swiper-item" style="background: #000">1</div>
<div class="swiper-item" style="background: #4269eb">2</div>
<div class="swiper-item" style="background: #247902">3</div>
</div>
</div>
然后再配上css样式:
.swiper {
position: relative;
width: 300px; /* 下面是为了让大家看的更清楚,加的修饰 */
padding: 30px 0;
margin: 0 auto;
background: #FFB973;
}
.swiper .swiper-container {
position: relative;
/* 为啥要设置-300px呢,因为我想让他默认在第二个滑块的位置,一会会给大家演示 */
left: -300px;
/* 让容器尽可能的宽,这样才能容纳更多的滑块 */
width: 10000%;
/* 让内部滑块可以排成一行 */
display: flex; /* 下面是为了让大家看的更清楚,加的修饰 */
background: red;
padding: 15px 0;
}
.swiper .swiper-container .swiper-item {
/* 宽度设置1%会按照外层视图的宽度来铺满 */
width: 1%;
height: 300px;
background: #eee; /* 下面是为了让大家看的更清楚,加的修饰 */
text-align: center;
font-size: 40px;
color: #fff;
}
你就会看到这么个效果:
当然,你可以把我加的修饰css样式都给去掉,然后再试试。
2.1.2 滑块的滑动原理是什么?
如果你能够理解上面的html结构的话,那我们就可以进行滑动的讲解了。(如果还不理解的话,那就继续往下看吧~或许会突然恍然大悟!)
上面我们提到了“滑块容器”这个概念,滑块的左右移动就是他来负责的
.swiper .swiper-container {
position: relative;
left: -300px;
}
因为滑块的宽度是和视图的宽度一样的,所以我们这里滑块的宽度是300px,那么我们把容器的left设置为-300px,就相当于向左移动了一个滑块的宽度,设置为-600px就表示向左移动了两个滑块的宽度,懂了吧,如果你想移动到某个滑块,那么只需要知道这个滑块的顺序(从0开始),然后乘以滑块宽度的相反数就行了,比如要移动到第三个滑块,他的顺序是2,那么就是2 * -300 = -600
看下面动图演示:
但是好像并没有出现滑动的动画效果耶,废话,还没写呢,有些童鞋可能喜欢用jquery
,习惯了他的animate动画方法,说实话其实我不太喜欢,因为我觉得css自带的动画完全可以解决大部分需求,而且当你以后用了vue这种mvvm框架,你会发现jquery这种动画方式很不实用!
扯远了,不过今天我们不用css的animation
属性,我们用另外一个属性transition
就可以满足,看名字你也能猜到,就是一个过渡属性,详细的用法请参考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions
我们把容器加上transition
属性试试看哈:
.swiper .swiper-container {
/* 省略... */
transition: left 0.2s ease-in-out;
}
所以,我们写的这段css代码transition: left 0.2s ease-in-out;
是表示:如果元素的left值变更,那么会有一个0.2s的过渡动画(补间动画)
到这里,我觉得你应该能理解了吧,每个滑块swiper-item
的左右滑动,并不是滑块本身在移动,而是他的父元素swiper-container
容器在左右移动(left值变化),然后我们用transition
属性来让这个变化过程出现一个过渡动画效果!
2.1.3 如何来触发滑动?
上面我们扯了一堆html和css,接下来我们说点js吧。
“如何来触发滑动?”,我们先不考虑手机端,就按照pc网页来,那么触发操作就是在容器上按住鼠标向左/右拖动,然后松开鼠标后,滑块就会向左/右滑动。
整个流程都跟鼠标事件挂钩:
mousedown
鼠标按下事件mousemove
鼠标移动事件mouseup
鼠标抬起事件
利用好这3个事件,我们就可以来实现鼠标控制滑块移动了!!我们先来实现摁住鼠标向左、向右拖动滑块。
既然我们的容器swiper-container
是负责左右移动的,那么我们就来监听他的鼠标事件吧,首先用querySelector
获取视图和容器两个元素节点:
// 首先获取视图层元素
const swiperEl = document.querySelector('.swiper');
// 在视图层里边查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container');
获取到容器元素后,就可以用他的addEventListener
来监听事件了:
containerEl.addEventListener('mousedown', (event) => {
console.log('鼠标按下了');
});
containerEl.addEventListener('mousemove', (event) => {
console.log('鼠标移动了');
});
containerEl.addEventListener('mouseup', (event) => {
console.log('鼠标抬起了');
});
看下动图操作:
虽然我们可以成功的监听到鼠标的操作事件,但是好像有点问题,我们期望的结果是,只有当鼠标按下后才会触发鼠标移动操作,但是现在看来并没有,所以可以考虑加一个状态来控制。
let state = 0; // 鼠标默认状态 containerEl.addEventListener('mousedown', (event) => {
state = 1; // 设置为1表示按下了鼠标
console.log('鼠标按下了');
});
containerEl.addEventListener('mousemove', (event) => {
if (state != 1) return; // 只有当state == 1时候才允许执行该事件
console.log('鼠标移动了');
});
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢复默认状态
console.log('鼠标抬起了');
});
这样就好多了!!!
那么鼠标事件有了,接下来要让容器跟着鼠标左右动才行。
我们要知道,浏览器对于鼠标的任何操作,都会有一个坐标参数(pageX和pageY),所以,我们可以根据鼠标移动时候的坐标参数来计算容器的left值,你可以想象一下,当你摁下鼠标然后左右移动,鼠标每次移动相对于上次都会产生一个距离,我们是不是可以把容器的left值加上或者减去这个距离,从而达到一个拖动效果呢?记得前面我们回调函数里边的event
参数了吗,他就是鼠标当前操作的相关属性,而我们目前只需要用到pageX属性
下面我们来写代码,有个地方需要注意下,我们先把容器的transition
这个属性给注释掉,后面会解释为什么?
.swiper .swiper-container {
/* 省略... */ /* transition: left 0.2s ease-in-out; */
}
每一步的操作,都在注释里边详细标注: // 首先获取视图层元素
const swiperEl = document.querySelector('.swiper');
// 在视图层里边查找容器元素
const containerEl = swiperEl.querySelector('.swiper-container'); let state = 0; // 鼠标默认状态
let oldEvent = null; // 用来记录鼠标上次的位置
// 获取容器的初始left值
let left = containerEl.offsetLeft; containerEl.addEventListener('mousedown', (event) => {
state = 1; // 设置为1表示按下了鼠标
oldEvent = event; // 当鼠标按下时候记录初始位置
console.log('鼠标按下了');
}); containerEl.addEventListener('mousemove', (event) => {
if (state != 1) return; // 只有当state == 1时候才允许执行该事件 // 用当前鼠标的位置来和上次鼠标的位置作比较
// 如果当前鼠标的pageX小于上次鼠标的pageX,那就表示鼠标在向左拖动,就需要把容器left值减去鼠标移动的距离
if (event.pageX < oldEvent.pageX) {
left -= oldEvent.pageX - event.pageX;
}
else {
left += event.pageX - oldEvent.pageX;
}
// 完事之后记得把当前鼠标的位置赋值给oldEvent
oldEvent = event;
// 最后再把left赋值给容器
containerEl.style.left = left + 'px';
console.log('鼠标移动了');
}); containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢复默认状态
console.log('鼠标抬起了');
});
运行看效果:
没毛病,你看这个鼠标,他又白又。。。
可是,可是,你这鼠标松开后,也没滑动到对应位置啊,额,额,前面我们不是讲了嘛,滑块顺序、滑块宽度还记得么?0 - 滑块顺序 * 滑块宽度就会移动到这个滑块,还记得不?
我们用index
来记录当前滑块的顺序
let index = 0; // 记录当前滑块的顺序(从0开始)
用itemWidth
来存储滑块的宽度
// 获取到所有的滑块元素
const itemEls = containerEl.querySelectorAll('.swiper-item');
// 获取到滑块的宽度
const itemWidth = itemEls[0].offsetWidth;
把我们的left
变量改一下,之前left
变量是直接获取容器元素的left值,现在我们要根据index
来计算
// let left = containerEl.offsetLeft;
// 存储容器的left,这里我们根据index来计算初始容器的left值
let left = 0 - itemWidth * index;
// 设置容器的初始位置
containerEl.style.left = left + 'px';
这样我们只需要修改index
变量的值,那么容器初始位置就会发生变化。
然后,我们在鼠标按下的时候,记录下坐标位置,在鼠标抬起的时候拿当前鼠标的位置和按下的位置作比较,来判断用户是向左划的,还是向右划的!
加一个变量,用来记录鼠标按下的参数,并且在鼠标按下的时候进行赋值!
let startEvent = null; // 用来记录鼠标按下时候的位置(最初位置) containerEl.addEventListener('mousedown', (event) => {
state = 1; // 设置为1表示按下了鼠标
startEvent = oldEvent = event; // 当鼠标按下时候记录初始位置
console.log('鼠标按下了');
});
那么鼠标抬起的时候,只需要和startEvent.pageX
做比较,就可以判断出左滑还是右滑,左滑我们让index + 1
,右滑就让index - 1
,最终我们通过index
再来计算left
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢复默认状态 // 鼠标抬起时候,和按下的坐标作比对,用来判断是向左滑动还是向右滑动
// 向左滑动那么就是要显示下一个滑块,所以index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
} left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠标抬起了');
});
是不是像那么回事了,不过怎么没滑动动画呢,还记得我们注释掉的那个transition
么,为什么要注释掉呢,因为只有在鼠标抬起的那一刻才需要滑动动画,左右拖动是根据鼠标位移距离来计算left,数值很小,完全不需要衔接动画,所以,我们先把注释掉那个transition
代码单独提取出来放到一个和swiper-container
同级的.move
类里边,当鼠标抬起的时候,我们把swiper-container
追加一个move
类就行。
.swiper .swiper-container.move {
transition: left 0.2s ease-in-out;
} containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢复默认状态 // 鼠标抬起时候,和按下的坐标作比对,用来判断是向左滑动还是向右滑动
// 向左滑动那么就是要显示下一个滑块,所以index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
} // 追加一个move样式
containerEl.className += ' move';
// 当过度动画结束后,一定要把这个类给移除掉
containerEl.addEventListener('transitionend', () => {
// 正则替换 \s+ 表示一个或多个空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
}) left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠标抬起了');
});
注意观察swiper-container
的dom节点:
仔细看上面的动图,第一个和最后一个滑动的时候是不是越界了,那么我们只需要判断index
就行,看代码:
containerEl.addEventListener('mouseup', (event) => {
state = 0; // 恢复默认状态
// 鼠标抬起时候,和按下的坐标作比对,用来判断是向左滑动还是向右滑动
// 向左滑动那么就是要显示下一个滑块,所以index要加1
if (event.pageX < startEvent.pageX) {
index ++;
}
else {
index --;
}
// 防止滑块越界
// 如果当前滑块是第一个,向右滑动后,回到第一个滑块
// 如果是最后一个,向左滑动后,回到最后一个滑块
if (index < 0) {
index = 0;
}
else if (index > itemEls.length - 1) {
index = itemEls.length - 1;
}
// 追加一个move样式
containerEl.className += ' move';
// 当过度动画结束后,一定要把这个类给移除掉
containerEl.addEventListener('transitionend', () => {
// 正则替换 \s+ 表示一个或多个空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
console.log('鼠标抬起了');
});
我们扩展出的三个问题,基本上都解决了。
而且到目前为止,其实你已经实现了一个基本的滑块功能了,只不过略显粗糙!
2.2 自动播放的原理
回到我们的功能列表,我们来看下第四条“自动播放”,第一个想到的是setInterval
setInterval(() => {
// 这个回调会每隔2秒执行一次
}, 2000);
所以,我们只需要在这个回调函数里边写上让滑块滑动的代码不就行了?
我们是用index
变量来控制当前滑块的,那么每隔2秒让index
加1,最后再根据index
计算出left
的值,不就可以了?
setInterval(() => {
// 默认向左滑动
index ++;
// 如果滑动到最后一个滑块,则回到第一个滑块
if (index > itemEls.length - 1) {
index = 0;
}
// 下面的代码跟我们鼠标抬起的事件的代码一样的,要不要考虑简单的封装一下?
// 追加一个move样式
containerEl.className += ' move';
// 当过度动画结束后,一定要把这个类给移除掉
containerEl.addEventListener('transitionend', () => {
// 正则替换 \s+ 表示一个或多个空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
})
left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
}, 2000);
关于重复逻辑的问题,我们会在第二部分写插件时候进行封装,这部分,我们只讲原理,当然如果你是个强迫症患者,可以自己试着封装个函数。
不过他老是这么自动播放也不是个事,有时候我想看看内容,还没看完呢,就自动划走了,所以,我们可以当鼠标放在容器上的时候,停止播放,鼠标移开后又恢复自动播放
mouseover
鼠标移动到某个元素上mouseout
鼠标在某个元素上移开
我们还是在容器上监听这两个事件,并用一个状态autoplay
来控制播放:
// 自动播放状态
let autoplay = true; setInterval(() => {
if (!autoplay) return;
// 默认向左滑动
index ++;
// 如果滑动到最后一个滑块,则回到第一个滑块
if (index > itemEls.length - 1) {
index = 0;
} // 追加一个move样式
containerEl.className += ' move';
// 当过度动画结束后,一定要把这个类给移除掉
containerEl.addEventListener('transitionend', () => {
// 正则替换 \s+ 表示一个或多个空白字符
containerEl.className = containerEl.className.replace(/\s+move/, '');
}) left = 0 - itemWidth * index;
containerEl.style.left = left + 'px';
}, 2000); containerEl.addEventListener('mouseover', () => {
// 鼠标移动到容器上,停止播放
autoplay = false;
});
containerEl.addEventListener('mouseout', () => {
// 鼠标从容器上移开,恢复播放
autoplay = true;
});
当然,还有其他的方法来控制自动播放,比如用clearInterval
函数等。
3. 结尾
至此,我们的原理都讲的差不多了,有遗漏的地方,还望指出,那么在第二部分,我会和大家一块来把写的杂七杂八的代码做一个封装,让我们的代码插件化,适应更多的场景。
作者:Mr_芝麻
链接:https://www.jianshu.com/p/22accec4d17b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
用原生js来写一个swiper滑块插件的更多相关文章
- 给Ionic写一个cordova(PhoneGap)插件
给Ionic写一个cordova(PhoneGap)插件 之前由javaWeb转html5开发,由于面临新技术,遂在适应的过程中极为挣扎,不过还好~,这个过程也极为短暂:现如今面临一些较为复杂的需求还 ...
- 用javascript写一个emoji表情插件
概述 以我们写的这个emoji插件为例,网上已经有一些相关的插件了,但你总感觉有些部分的需求不能被满足(如:可以自行添加新的表情包而不用去改源代码等等) 详细 代码下载:http://www.demo ...
- Skywalking-02:如何写一个Skywalking trace插件
如何写一个Skywalking trace插件 javaagent 原理 美团技术团队-Java 动态调试技术原理及实践 类图 实现 ConsumeMessageConcurrentlyInstrum ...
- js单行写一个评级组件
单行写一个评级组件:"★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate); -----------------------------------分隔符- ...
- 写一个Vue loading 插件
什么是vue插件? 从功能上说,插件是为Vue添加全局功能的一种机制,比如给Vue添加一个全局组件,全局指令等: 从代码结构上说,插件就是一个必须拥有install方法的对象,这个方法的接收的第一个参 ...
- 改变滚动条的原始样式: chrome 可以改变, IE只能变相关颜色,firfox好像也不好改。最好是自己写一个或是用插件
相关作者链接地址: https://www.lyblog.net/detail/314.html 问题: 1.我在项目中遇到的问题: 在设置了::-webkit-scrollbar 后,滚动条不见了! ...
- 【原生JS】写最简单的图片轮播
非常简单的一个大图轮播,通过将控制显示位置来进行轮播效果,写来给正在学习的新手朋友们参考交流. 先看效果:(实际效果没有这么快) 先看布局: <div id="display" ...
- 原生js来写获取元素距离顶部距离,以及滚动条滚动指定距离和时间控制
这是我在写vue项目里封装的一个公共js类 里面还有一些其他的方法,一并拿过来了 class Public { isDesktop(){ //判断是否为pc端 return (window.scree ...
- 用原生JS实现的一个导航下拉菜单,下拉菜单的宽度与浏览器视口的宽度一样(js+html+css)
这个导航下拉菜单需要实现的功能是:下拉菜单的宽度与浏览器视口的宽度一样宽:一级导航只有两项,当鼠标移到一级导航上的导航项时,相应的二级导航出现.在本案例中通过改变二级导航的高度来实现二级导航的显示和消 ...
随机推荐
- Break 和 Continue 标签 kotlin(12)
Break 和 Continue 标签 在 Kotlin 中任何表达式都可以用标签(label ) 来标记. 标签的格式为标识符后跟 @ 符 号,例如: abc@ . fooBar@ 都是有效的标签( ...
- mysq乱码问题
不乱码的思想 liunx字符集→linux客户端字符集(例如:ssh)→mysql客户端字符集→mysql服务端字符集→库的字符集→表的字符集→程序字符集统一 mysql表跟库,库跟服务端字符集 li ...
- [go]从os.Stdin探究文件类源码
咋一看go的标准输入输出函数有一箩筐. 细究了一下. - 从标准输入获取输入 fmt.Scan 以空白(空格或换行)分割,值满后无结束 fmt.Scanln 以空格作为分割,遇到换行结束 fmt.Sc ...
- Zabbix - 配置服务器对第三方服务的监控
需求: 需要配置zabbix监控,使得zabbix服务器可以监控到另一台服务器上运行的第三方服务的状态,当状态异常时发送告警邮件. 限制:被监控的服务器不允许安装任意客户端,且该台服务器不能联通外网 ...
- gateway 整合 websocket demo
背景: 这个websocket 因为使用的地方不多,并没有独立出一个项目,是集成在已有的服务中. 1: gateway 配置 - id: service-test uri: lb:ws://se ...
- 使用Laravel首次运行出现 No application encryption key has been specified.
先观察Laravel根目录下是否有 .env文件,有则看1,无则看2 1.若直接报500错误: 输入命令: php artisan key:generate 2.若文件根目录下没有 .env文件 (1 ...
- Android Stuido中断点调试和高级调试
写一个简单的调试程序 import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public class M ...
- 小D课堂 - 新版本微服务springcloud+Docker教程_1_02技术选型
笔记 2.技术选型和学后水平 简介:课程所需基础和技术选型讲解,学完课程可以到达怎样的程度, 1.IDEA JDK8 Maven SpringBoot基础 Linux 2.理 ...
- Git代码行数统计命令
统计zhangsan在某个时间段内的git新增删除代码行数 git log --author=zhangsan--since=2018-01-01 --until=2019-04-01 --forma ...
- MYSQL查询今天、昨天、7天前、30天、本月数据
今天: SELECT * FROM 表名 WHERE TO_DAYS( 时间字段名) = TO_DAYS(NOW()); 昨天: SELECT * FROM 表名 WHERE TO_DAYS( NOW ...