前言:
近期我在项目中就接到了一个完成轮播图组件的需求。最开始我也像大家一样,直接选择使用了知名的开源项目 “Swiper”,但是后来发现它在移动端项目中某些测试环境下会白屏一段时间。无论如何调试都不能修复这个问题,于是就自己上手写了个轮播图组件,实现代码其实也只有 200 行,很少但是完美解决了我们项目的问题。
虽然已经 2023 年了,但是轮播图组件的实现仍然是考验前端基本功的经久不衰的题目,于是来分享一下实现思路。🎁
tips: 本文主要目的不是一上来就贴代码,而是会一步一步带你理清细节部分,即使你现在没有轮播图这个需求。
一. 使用 overflow-scroll 完成基础框架
-
大家在项目中肯定接触到溢出滚动的需求,其实就是用到了 overflow-auto 等相关属性。
-
注意:样式方面,在这里我使用的是
UnoCSS
,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过UnoCSS
,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。
🫱手把手教你如何创建一个代码仓库 -
让我们快速制造一个溢出的场景来完成准备工作。其实非常简单,就是简单的创造一个容器,容器里放着三个和容器宽高相同的 div。然后给父容器一个 overflow-auto 属性,让它可以在内容溢出的时候发生滚动。
效果如下:
二. 实现合适位置自动切换
-
现在我们仅仅实现了一个可以滚动的容器而已,但是轮播图最主要的事情就是用户滚动的位置不合适,那么我们也要自动调整到合适的位置显示。
-
更具体来讲,就是当我们拖动图片到了中间这样的位置松手时,轮播图最重要一个功能就是可以自动切换到上一张或者下一张,准确的显示合适的内容给用户。(因为展示内容区域展示一半一半的内容毫无意义嘛。)
-
这里就需要用到两个至关重要的 CSS 属性,
- snap-type
- snap-align
我们先看 snap 这个单词的意思。在这里它的意思我认为 “咔嚓一声,折断” 更符合这个属性的含义,不要着急,你可以暂时先带着这个模糊的概念来慢慢理解接下来的内容。
-
我们先看 snap-type 是用来干什么的。
这里 MDN 的解释不是特别好懂,接下来我会用人话翻译一下它想表达的含义。 -
回到我们的代码部分,我们创建了一个容器 div,并且这个容器因为溢出,并且设置了 overflow-滚动 相关属性。
其实我们的 scroll-type 是用来给滚动容器的!这里特别注意,它一定是用在设置了 overflow-auto 等属性的那个元素上的。 -
关于属性值,我们采用 snap-type: x mandatory。
在这里x
的含义代表着横轴发生的滚动,那么聪明的你可以猜到,它也有一个y
属性,代表着竖轴发生滚动时的设置。 -
这里还有一个关键字
mandatory
代表着强制的意思。因为在某些情况下,浏览器会认为用户滑到下面这种位置是自愿的,但是我们的场景是不需要考虑这种情况的,所以要告诉浏览器我们需要你帮我 “强制咔嚓折断”。
-
至于 proximity ,这个属性的算法有点奇怪,我也没太搞懂它的含义,不过我们的需求不需要用到这个关键字,大家有兴趣也可以自行查阅。
-
接下来给我们的容器设置这个属性,让我们先看看效果。
-
什么情况?怎么没效果呢?目前为止我们仅仅给容器设置了滚动的需求还是不够的,还得告诉子元素滚动到什么位置停下才行。这里就需要用到
snap-align
属性了。它是给滚动容器的子元素设置的。 -
snap-align
,这个属性有三个值可以设置,none
,center
和end
。
其实从这个属性的名字就可以猜到,它其实设置的是子元素的位置是相对于滚动容器的左边对齐还是右边对齐。 -
怎么理解呢?我们将滚动容器的宽度调大,让它可以漏出一点点其它元素的内容。并且只给数字2的元素设置 scroll-align 相关属性。(tips: 这里需要重点注意,我们只给了一个元素设置了这个属性,另外两个元素是没有设置这个属性的。)
-
snpa-align: start (元素2无论如何都会在松开鼠标的时候紧贴着滚动容的左边,也就是滚动容器的 start 位置
-
snap-align: center (元素2无论如何都会紧贴着滚动容器中间位置
-
snap-align: end (元素2的右边无论如何都会紧贴着滚动容器
-
-
知道了这三个属性的区别,那么接下来复原我们的容器样子,因为我们实际上轮播图的每一项的宽高和滚动容器的内容区是恰好相等的,所以我们给子元素无论设置怎样的三个值的效果都是一样的。
让我们看一下效果:
看起来效果还不错~
三. 实现上一项和下一项切换功能
-
我们准备两个按钮,当用户点击这两个按钮的时候,可以进行手动的切换上一张和下一张。
-
这里我们需要用到滚动容器的 scroll 事件,需要给滚动容器绑定相对的回调函数。
这里通过e.target
就可以拿到我们滚动容器本身。容器自身存在一个 scollLeft 属性,你需要知道一个知识点其实发生滚动的本质就是 scrollLeft 值的变化。
注意观看下面滚动容器的 scrollLeft 属性值的变化。
-
知道了这个关键点,那么我们的
pre
和next
函数就可以很明确的书写了。
首先通过 ref 拿到元素本身。
-
然后在
pre
函数内部获取当前滚动元素的scollLeft
值。
-
紧接着,你需要知道的是,这个值即是一个可读属性,也是一个可写属性。那么我们就可以进行判断,如果当前照片不是第一张的话。,那么我就让 scrollLeft 的值 -300。这里有两个关键的知识点。
-
- 第一张对应的 scrollLeft 等于0
-
- 这里的 300 是我们写死的宽度,你可以根据后面自己的项目优化这个值。
-
-
让我们看一下效果,先让我们手动滚动到第三张,然后点击上一张切换。
-
这里好像有一点点不对劲,我们不是平缓过度到上一张的而是直接切换到上一张的,这里很简单,需要给滚动容器设置一个
scroll-behavior:smooth
即可。
我们看一下效果:
-
下一张按钮的实现同理,这里不过多赘述,代码如下:
-
最终的效果:
四. 源码
<script setup >
import { ref, computed } from "vue";
const box = [
{
number: 1,
bg: "blue",
},
{
number: 2,
bg: "pink",
},
{
number: 3,
bg: "red",
},
];
const containerEl = ref<HTMLDivElement>();
function scrollEvent(e: UIEvent) {
const containerEl = e.target as HTMLDivElement;
}
// 上一张
function pre() {
const el = containerEl.value;
if (!el) return;
const scrollLeft = el?.scrollLeft;
if (scrollLeft > 0) {
el.scrollLeft = scrollLeft - 300;
}
}
function next() {
const el = containerEl.value;
if (!el) return;
const scrollLeft = el?.scrollLeft;
const max = (box.length - 1) * 300; //轮播图的数量 -1
if (scrollLeft < max) {
el.scrollLeft = scrollLeft + 300;
}
}
</script>
<template>
<div
class="w-100vw h-100vh text-14px text-black flex justify-center items-center"
>
<div
@click.stop="pre"
class="w-60px h-60px rounded-full bg-black flex items-center justify-center"
>
<span class="text-white">上一张</span>
</div>
<div
ref="containerEl"
@scroll="scrollEvent"
class="w-300px h-300px overflow-auto flex snap-x snap-mandatory scroll-smooth"
>
<div
v-for="(item, index) in box"
class="w-300px h-300px shrink-0 leading-300px text-center snap-center"
:style="{ backgroundColor: item.bg }"
>
<span class="text-100px text-white">{{ item.number }}</span>
</div>
</div>
<div
@click="next"
class="w-60px h-60px rounded-full bg-black flex items-center justify-center"
>
<span class="text-white">下一张</span>
</div>
</div>
</template>
五. 结语
在真正的项目上,其实我还实现了从最后一张到第一张,或者从第一张到最后一张的无缝切换,但是讲解起来稍微有点复杂,会让文章变得冗长。所以计划本文只是讲解基础框架,如果有读者希望学习思路的话,我会在之后更新相关知识点🎁。