嗨~ 这里是芝麻, 今天我们一块来做一个 "滑块插件". 那么啥是滑块插件呢? 滑块插件能干嘛呢? 请看下图:
是不是有点印象了, 没错, 他的最基本的用法就是左右滑动, 插件使用者只需要写几行简单的 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('鼠标抬起了');
- });
鼠标按下抬起事件 2
这样就好多了!!!
那么鼠标事件有了, 接下来要让容器跟着鼠标左右动才行.
我们要知道, 浏览器对于鼠标的任何操作, 都会有一个坐标参数 (pageX 和 pageY), 所以, 我们可以根据鼠标移动时候的坐标参数来计算容器的 left 值, 你可以想象一下, 当你摁下鼠标然后左右移动, 鼠标每次移动相对于上次都会产生一个距离, 我们是不是可以把容器的 left 值加上或者减去这个距离, 从而达到一个拖动效果呢? 记得前面我们回调函数里边的 event 参数了吗, 他就是鼠标当前操作的相关属性, 而我们目前只需要用到 pageX 属性
event
下面我们来写代码, 有个地方需要注意下, 我们先把容器的 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('鼠标抬起了');
- });
运行看效果:
鼠标拖动. gif
没毛病, 你看这个鼠标, 他又白又...
可是, 可是, 你这鼠标松开后, 也没滑动到对应位置啊, 额, 额, 前面我们不是讲了嘛, 滑块顺序, 滑块宽度还记得么? 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('鼠标抬起了');
- });
[图片上传失败...(image-640fb6-1558943326062)]
是不是像那么回事了, 不过怎么没滑动动画呢, 还记得我们注释掉的那个 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 节点:
[图片上传失败...(image-39e508-1558943326062)]
仔细看上面的动图, 第一个和最后一个滑动的时候是不是越界了, 那么我们只需要判断 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('鼠标抬起了');
- });
[图片上传失败...(image-92a2ce-1558943326062)]
我们扩展出的三个问题, 基本上都解决了.
而且到目前为止, 其实你已经实现了一个基本的滑块功能了, 只不过略显粗糙!
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);
[图片上传失败...(image-34cc43-1558943326062)]
关于重复逻辑的问题, 我们会在第二部分写插件时候进行封装, 这部分, 我们只讲原理, 当然如果你是个强迫症患者, 可以自己试着封装个函数.
不过他老是这么自动播放也不是个事, 有时候我想看看内容, 还没看完呢, 就自动划走了, 所以, 我们可以当鼠标放在容器上的时候, 停止播放, 鼠标移开后又恢复自动播放
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;
- });
[图片上传失败...(image-3e51f7-1558943326062)]
当然, 还有其他的方法来控制自动播放, 比如用 clearInterval 函数等.
3. 结尾
至此, 我们的原理都讲的差不多了, 有遗漏的地方, 还望指出, 那么在第二部分, 我会和大家一块来把写的杂七杂八的代码做一个封装, 让我们的代码插件化, 适应更多的场景.
喜欢的童鞋, 粉一下呗~, 不定时各种技术教程更新
来源: http://www.jianshu.com/p/22accec4d17b