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

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

支持 slidesPerViewspaceBetween、滑动距离决定滑动数量,代码仅 400 行,核心原理全解析。

引言

在业务开发中,轮播图是几乎每个前端都会遇到的场景。Swiper 无疑是功能最全面的库,但它体积较大(核心库 ~30kB,加上模块更重),且在某些轻量化项目中显得有些"杀鸡用牛刀"。因此,我决定用 Vue 3 + TypeScript 手写一个轮播组件,只保留最常用的 NavigationPagination,同时支持多视图(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 时置为 truetouchend 时重置(延迟一帧)。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 依然是标杆。希望这篇文章能给您带来启发!


组件代码仓库:可在评论区留言获取完整源码。

相关推荐
牛马1112 小时前
Flutter BoxDecoration border 完整用法
开发语言·前端·javascript
CodeSheep2 小时前
宇树科技的最新工资和招人标准
前端·后端·程序员
奔跑的卡卡2 小时前
Web开发与AI融合-第二篇:TensorFlow.js实战:在浏览器中运行AI模型
前端·人工智能·tensorflow
IT_陈寒2 小时前
Vue的响应式居然在这里埋坑,差点加班到天亮
前端·人工智能·后端
We་ct2 小时前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·javascript·算法·leetcode·typescript
jarvisuni2 小时前
JCode添加批量测试,一键同步运行6个Claude Code!
java·服务器·前端
小李子呢02113 小时前
前端八股CSS(3)---水平垂直居中的实现方法
前端·css·css3
M ? A3 小时前
Vue3 转 React:组件透传 Attributes 与 useAttrs 使用详解|VuReact 实战
前端·javascript·vue.js·经验分享·react.js·开源·vureact
火山引擎开发者社区3 小时前
方舟 Coding Plan 支持 Embedding 模型,让 AI Agent “找得更准、记得更久”
前端·javascript·人工智能