一个轻量级 Vue3 轮播组件:支持多视图、滑动距离决定切换数量,核心原理与 Swiper 对比

支持
slidesPerView、spaceBetween、滑动距离决定滑动数量,代码仅 400 行,核心原理全解析。
引言
在业务开发中,轮播图是几乎每个前端都会遇到的场景。Swiper 无疑是功能最全面的库,但它体积较大(核心库 ~30kB,加上模块更重),且在某些轻量化项目中显得有些"杀鸡用牛刀"。因此,我决定用 Vue 3 + TypeScript 手写一个轮播组件,只保留最常用的 Navigation 和 Pagination,同时支持多视图(slidesPerView)和间距(spaceBetween),并实现"根据滑动距离决定切换数量"的自然交互。
本文会详细讲解实现原理、核心难点,并与 Swiper 进行对比,希望能给正在造轮子或想深入理解轮播机制的你一些启发。
组件特性
- ✅ 多视图模式 :通过
slidesPerView控制每屏显示几张幻灯片 - ✅ 可配置间距 :
spaceBetween设置幻灯片之间的间隔 - ✅ 循环播放:无缝无限滚动,复制首尾元素实现
- ✅ 自动播放:支持悬停暂停
- ✅ 拖拽滑动:鼠标/触摸拖拽,根据滑动距离(四舍五入)决定一次滑动的 slide 数量,而非固定 1 张
- ✅ 导航与分页 :分页器在非循环模式下显示可滑动步数(
总条数 - 每屏个数 + 1) - ✅ 点击事件:区分拖拽与点击,避免误触发
- ✅ TypeScript:完整类型定义,便于接入大型项目
实现原理
1. 多视图与间距的布局计算
核心思路:使用 flex 布局,每个 slide 的宽度动态计算,右外边距实现间距。
js
const slideWidth = (containerWidth - (slidesPerView - 1) * spaceBetween) / slidesPerView;
const slideStep = slideWidth + spaceBetween; // 每次滚动的总步长
滚动时通过 transform: translate3d(-currentOffset * slideStep, 0, 0) 移动整个轨道。
2. 循环模式(Loop)的实现
真正的无限循环不是把数据无限复制,而是在原始数组前后各复制 slidesPerView 个 slide,形成"假首尾"。初始时偏移量设为复制品的起始位置。当用户滑动到复制品区域时,过渡结束后立即无动画跳转到对应的真实 slide,视觉上无感知。
关键步骤:
displaySlides=[...clonesFront, ...originals, ...clonesBack]displayOffset=cloneCount + activeIndex(循环模式)或activeIndex(非循环)- 过渡结束后检测
displayOffset是否小于cloneCount或大于cloneCount + originals.length - 1,若是则修正activeIndex并重置位置。
注意 :分页器在循环模式下仍显示原始数据条数,change 事件始终返回原始索引。
3. 根据滑动距离决定滑动数量
很多简单轮播只支持一次滑动一张,体验呆板。我们希望像 Swiper 那样:拖拽超过半个 slide 宽度就切换,且滑动距离越大,一次切换的张数越多。
实现方法:
- 拖拽结束时计算
deltaSlides = Math.round(dragDistance / slideStep) - 目标索引 =
currentIndex - deltaSlides(向右滑动为正,索引减少) - 调用
goTo(newIndex),内部自动处理边界和循环取模。
4. 分页器点数计算
这是许多开发者容易出错的地方。假设有 20 张图,每屏显示 3 张,那么分页器应该有几个点?
- 非循环模式 :用户可以滑动到的不同起始索引有
20 - 3 + 1 = 18个位置,因此分页器应为 18 个点,每个点代表一组可见 slide。 - 循环模式:由于可以无限滚动,分页器仍然显示 20 个点,对应原始数据的索引。
组件中通过 maxStartIndex = slides.length - slidesPerView 计算最大起始索引,paginationCount = loop ? slides.length : maxStartIndex + 1。
5. 拖拽与点击的区分
直接给 slide 绑 @click 会导致拖拽结束后也触发点击。解决方案:在 touchstart/mousedown 时设置 dragOccurred = false,在 touchmove 中检测移动距离超过 5px 时置为 true,touchend 时重置(延迟一帧)。click 事件检查该标志,若为 true 则忽略。
6. 自动播放与性能优化
- 自动播放使用
setInterval,在用户交互(拖拽、点击导航)时重置定时器。 - 窗口
resize时重新计算宽度并修正位置。 - 使用
will-change: transform开启 GPU 加速。
与 Swiper 的对比
| 维度 | 本组件 | Swiper |
|---|---|---|
| 体积 | ~400 行源码,无依赖 | 核心 ~30KB,完整功能 ~70KB+ |
| 功能覆盖 | Navigation, Pagination, 多视图, 循环, 自动播放, 拖拽滑动数量 | 所有你能想到的轮播功能(缩略图、3D 流、懒加载、RTL 等) |
| 学习成本 | 极低,Props 直观 | 配置项丰富,需要查阅文档 |
| 扩展性 | 简单,可自由修改源码 | 通过模块和 API 扩展,但定制复杂功能仍需理解内部机制 |
| TypeScript | 原生 TS 编写,类型完整 | 有 @types/swiper,但配置项类型复杂 |
| 移动端适配 | 支持触摸,已处理被动事件 | 专业级,手势非常顺滑 |
| 维护性 | 个人项目,需自行维护 | 社区维护,更新及时 |
| 适用场景 | 轻量级项目、特定场景、学习目的 | 企业级、复杂交互、追求稳定全面 |
总结:如果你的项目只需要基础轮播且对体积敏感,或者你想完全掌控交互细节,这个组件是很好的选择;如果需要支持 IE、复杂手势或特殊效果,Swiper 仍是首选。
组件使用示例
vue
<template>
<Carousel
:slides="banners"
:slidesPerView="3"
:spaceBetween="20"
:loop="true"
:autoplay="true"
@slide-click="onClick"
>
<template #slide="{ item }">
<div class="card">
<img :src="item.url" />
<p>{{ item.title }}</p>
</div>
</template>
</Carousel>
</template>
核心代码片段
拖拽滑动数量计算
ts
const endDrag = () => {
const deltaSlides = Math.round(dragDelta.value / slideStep.value);
if (deltaSlides !== 0) {
goTo(activeIndex.value - deltaSlides);
} else {
// 回弹
wrapperRef.value.style.transform = `translate3d(${translateDistance.value}px, 0, 0)`;
}
};
循环修正
ts
const performLoopCorrection = () => {
const offset = displayOffset.value;
const min = cloneCount.value;
const max = cloneCount.value + slidesLength.value - 1;
if (offset < min) {
activeIndex.value += slidesLength.value;
jumpToOffset(cloneCount.value + activeIndex.value, true);
emit('loop-correct', activeIndex.value);
} else if (offset > max) {
activeIndex.value -= slidesLength.value;
jumpToOffset(cloneCount.value + activeIndex.value, true);
emit('loop-correct', activeIndex.value);
}
};
总结
造轮子不是为了重复发明,而是为了深入理解。通过实现这个轮播组件,我掌握了多视图布局、循环复制的技巧、拖拽距离映射滑动数量、分页器正确计数等核心知识。相比直接使用 Swiper,这个组件让我的 Vue 能力提升了一个台阶。
如果您的项目需要轻量级、可定制的轮播,不妨试试这个组件;如果您需要更全面的功能,Swiper 依然是标杆。希望这篇文章能给您带来启发!
组件代码仓库:可在评论区留言获取完整源码。