不到200行用Vue实现类似Swiper.js的轮播组件

  • 不到200行用Vue实现类似Swiper.js的轮播组件已关闭评论
  • 96 次浏览
  • A+
所属分类:Web前端
摘要

大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等。


前言

大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等。

本文将详细讲解如何用Vue一步步实现的类似Swiper.js的功能,无任何第三方依赖,干货满满。

最终效果

在线预览:https://zyronon.github.io/douyin/

项目源代码:https://github.com/zyronon/douyin

注意PC 必须将浏览器切到手机模式,先按 F12 调出控制台,再按 Ctrl+Shift+M才能正常预览

Demo代码

上面的预览地址是最终实现的效果,下面才是本文代码实现的效果

为提升阅读体验,正文中代码展示有部分省略处理,完整代码可以在codesandbox上查看:
https://codesandbox.io/p/devbox/mutable-grass-zm4gl5

实现原理

布局

我们需要用到两个div,父元素 slide 设置 overflow: hidden 禁止滚动,子元素 slide-list 使用 flex 布局,然后将需要滚动的页面做为孙元素放在子元素 slide-list 中,由于子元素 slide-listflex 布局,页面会自然的平铺排列

因为父元素 slideoverflow: hidden属性会将内容裁减,不提供滚动条,也不允许用户滚动,所以我们只能看到父元素 slide 宽高的内容。

不到200行用Vue实现类似Swiper.js的轮播组件

<div class="slide">   <div class="slide-list">     <slot></slot>   </div> </div> 
.slide {   touch-action: none;   height: 100%;   width: 100%;   transition: height 0.3s;   position: relative;   overflow: hidden; }   .slide-list {     height: 100%;     width: 100%;     display: flex;     position: relative;   } 

滑动

实现滚动的关键点在于CSS3transform: translate(0, 0) 属性。

translate()  这个 CSS 函数在水平和/或垂直方向上重新定位元素,它的坐标定义了元素在每个方向上移动了多少。

因为子元素 slide-list 的内容是平铺的,我们只需要在子元素 slide-list 监听对应的事件,计算滑动的距离xy,再动态设置到子元素 slide-listtransform: translate(x, y)里面,就可以实现页面滑动了

总结

大家可以将整个流程理解为播放胶片电影:父元素 A 是放映机,子元素 B 是胶片,而页面是印刷在胶片上的内容。胶片每移动一格,我们就能看到新的一帧电影

不到200行用Vue实现类似Swiper.js的轮播组件

实现

监听事件

PC 上的点击、移动,H5 的手势操作,都离不开 DOM 事件监听。例如鼠标移动事件对应 mousemove,移动端因为没有鼠标则对应 touchmove
我们可以通过 Pointer 事件进行多端统一的事件监听,实现触屏和 PC 端通用

不到200行用Vue实现类似Swiper.js的轮播组件

<div class="slide horizontal">   <div       class="slide-list"       ref="wrapperEl"       @pointerdown="onPointerDown"       @pointermove="onPointerMove"       @pointerup="onPointerUp"   >     <slot></slot>   </div> </div> 

初始化

组件默认变量

//slide-list的ref引用 const wrapperEl = ref(null)  const state = reactive({   judgeValue: 20,//一个用于判断滑动朝向的固定值   type: SlideType.VERTICAL,//组件类型   name: props.name,   localIndex: props.index,//当前下标   needCheck: true,//是否需要检测,每次按下都需要检测,up事件会重置为true   next: false,//能否滑动   isDown: false,//是否按下,用于move事件判断   start: {x: 0, y: 0, time: 0},//按下时的起点坐标   move: {x: 0, y: 0},//移动时的坐标   wrapper: {width: 0, height: 0, childrenLength: 0}//slide-list的宽度和子元素数量 }) 
function slidePointerDown(e, el, state) {   Utils.$setCss(el, 'transition-duration', `0ms`)   //记录起点坐标,用于move事件计算移动距离   state.start.x = e.pageX   state.start.y = e.pageY   //记录按下时间,用于up事件判断滑动时间   state.start.time = Date.now()   state.isDown = true } 

虽然我们用 Pointer事件统一了移动端和PC端的监听事件,但 pointermove 事件在 PC 和移动端表现出来的效果却不一样,在 PC 上, pointermove 事件和 mousemove 事件一致,只要鼠标在目标元素上方,就会触发。而在移动端上却只有按下并移动时发才会触发

所以这里用一个 isDown 的变量保存是否按下的状态,pointermove事件虽然会一直触发,但仅当 isDown 时才执行我们的代码逻辑

移动过程

function slidePointerMove(e,el,state) {   if (!state.isDown) return;    //计算移动距离   state.move.x = e.pageX - state.start.x   state.move.y = e.pageY - state.start.y    //检测能否滑动   let canSlideRes = canSlide(state)    //是否是往下(右)滑动   let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0    if (canSlideRes) {     if (canNext(state, isNext)) {       //能滑动,那就把事件捕获,不能给父组件处理       Utils.$stopPropagation(e)        //获取偏移量       let t = getSlideOffset(state, el) + (isNext ? state.judgeValue : -state.judgeValue)       let dx1 = 0,         dx2 = 0       //偏移量加当前手指移动的距离就是slide要偏移的值       if (state.type === SlideType.HORIZONTAL) {         dx1 = t + state.move.x       } else {         dx2 = t + state.move.y       }       Utils.$setCss(el, 'transition-duration', `0ms`)       Utils.$setCss(el, 'transform', `translate(${dx1}px, ${dx2}px)`)     }   } } 

用鼠标当前的位置,再减去鼠标按下时的位置,就是鼠标移动的距离
移动距离再加上当前页面 * 每个页面的宽或高,即子元素 slide-list 整体要偏移的量

技术难点

1. 如何判断滑动方向?是在上下滑还是左右滑?
//检测在对应方向上能否允许滑动,比如SlideHorizontal组件就只处理左右滑动事件,SlideVertical  //只处理上下滑动事件  export function canSlide(state) {   //每次按下都需要检测,up事件会重置为true   if (state.needCheck) {     //判断move x和y的距离是否大于判断值,因为距离太小无法判断滑动方向     if (Math.abs(state.move.x) > state.judgeValue || Math.abs(state.move.y) > state.judgeValue) {       //放大再相除,根据长宽比判断方向,angle大于1就是左右滑动,小于是上下滑动       let angle = (Math.abs(state.move.x) * 10) / (Math.abs(state.move.y) * 10)       //根据当前slide的类型,判断能否滑动,并记录下来,后续不再判断,直接返回记录值       state.next = state.type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1       state.needCheck = false     } else {       return false     }   }   return state.next } 

放大移动距离后再相除,根据结果是否大于1判断出滑动方向

2. 如何处理嵌套组件中的事件冲突?什么时候拦截事件和放行事件?

由于事件的冒泡机制,事件是从最里面的元素一级一级的往上冒泡的,所以我们只需在满足下面两个条件时拦截事件即可

  1. 是否在往到头或尾滑动
    如果在第一页,不能往左/上滑动
    如果在最后一面, 不能往右/下滑动
function canNext(state, isNext) {   return !(     (state.localIndex === 0 && !isNext) ||     (state.localIndex === state.wrapper.childrenLength - 1 && isNext)   ) } 
  1. 滑动方向和组件类型相匹配
  • SlideHorizontal.vue 组件只允许向左/右滑动
  • SlideVertical.vue 组件只允许向上/下滑动

满足上述两个条件时拦截事件,不满足放行事件,交给上一级组件处理

//检测在对应方向上能否允许滑动 let canSlideRes = canSlide(state) //是否是往下(右)滑动 let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0 if (canSlideRes) {   if (canNext(state, isNext)) {     //能滑动,那就把事件捕获,不能给父组件处理     Utils.$stopPropagation(e)     ...     滑动逻辑     ...   } } 

结束滑动

function slidePointerUp(e, state) {   if (!state.isDown) return;   let isHorizontal = state.type === SlideType.HORIZONTAL   let isNext = isHorizontal ? state.move.x < 0 : state.move.y < 0   if (state.next) {     if (canNext(state, isNext)) {       //结合时间、距离来判断是否成功滑动       let endTime = Date.now()       let gapTime = endTime - state.start.time       let distance = isHorizontal ? state.move.x : state.move.y       let judgeValue = isHorizontal ? state.wrapper.width : state.wrapper.height       //1、距离太短,直接不通过       if (Math.abs(distance) < 20) gapTime = 1000       //2、距离太长,直接通过       if (Math.abs(distance) > judgeValue / 3) gapTime = 100       //3、若不在上述两种情况,那么只需要判断时间即可       if (gapTime < 150) {         if (isNext) state.localIndex++         else state.localIndex--       }     }   }    // 重置变量   Utils.$setCss(el, 'transition-duration', `300ms`)   let t = getSlideOffset(state, el)   let dx1 = 0,dx2 = 0   if (state.type === SlideType.HORIZONTAL) dx1 = t   else dx2 = t   Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)   ... } 

技术难点

  • 如何让滑动结束时的动画更丝滑?
    结合滑动时间、滑动距离来判断滑动下一条还是保持当前条
    1、距离太短,直接不通过
    2、距离太长,直接通过
    3、若不在上述两种情况,那么只需要判断时间即可,小于150毫秒以内就算是成功滑动

其他问题

PC 上滑动有图片的页面,图片“分叉”了:我们开始拖动它的“克隆”

这是因为浏览器有自己的对图片和一些其他元素的拖放处理。它会在我们进行拖放操作时自动运行,并与我们的拖放处理产生了冲突

禁用它:

@dragstart="(e) => Utils.$stopPropagation(e)" 

PC 上滑动结束后触发了click事件

问题分析

首先我们滑动是利用 pointerdown, pointermove, pointerup 三个事件组合形成的,但是 pointerup 执行之后, click 是一定会执行的,是无法避免的,是无法用preventDefault , stopPropagation , stopImmediatePropagation 阻止的, 因为pointer 事件和 click 事件本身就不是一个系列的,因此没有关系,所以当发生滑动之后,pointerup 一定会执行,click 也会在 pointerup 执行后执行

解决方案

我们设置一个全局变量

window.isMoved = false 

pointermove 事件中,将 window.isMoved 设为 true。然后在 pointerup 事件中,我们用一个定时器让这个变量在200毫秒之后发生改变为 false,因为 pointerup 之后 click 很快就触发了,不到200ms,因此可以保证变量还没有发生变化,click 事件里面去检测这个变量,如果是变化之前,那么不执行

如果 click 事件少还好说,直接复制几遍无所谓。
但是一般来说 click 事件在项目中使用还是挺多的,有没有什么一劳永逸的办法呢?
大部分监听 click 事件都是用 Vue@click 添加的,我们无法插手

这时给大家介绍一下 Proxy 这个对象了,Vue3 的双向绑定就用到了 Proxy 对象。
在项目入口,我们直接代理 HTMLElement.prototype.addEventListener 这个事件,代理了之后,Vue@click 语法糖添加事件时就会通知我们,这时再进行判断是不是 click 事件,是的话再判断 window.isMoved 的状态

window.isMoved = false HTMLElement.prototype.addEventListener = new Proxy(HTMLElement.prototype.addEventListener, {   apply(target, ctx, args) {     const eventName = args[0]     const listener = args[1]     if (listener instanceof Function && eventName === 'click') {       args[1] = new Proxy(listener, {         apply(target, ctx, args) {           if (window.isMoved) return           try {             return target.apply(ctx, args)           } catch (e) {             console.error(`[proxyPlayerEvent][${eventName}]`, listener, e)           }         }       })     }     return target.apply(ctx, args)   } }) 

设置了 overflow: auto 的页面在移动端不触发 pointermove 事件

再设置一个 touch-action:pan-y 就正常了

CSS 属性 touch-action 用于设置触摸屏用户如何操纵元素的区域 (例如,浏览器内置的缩放功能), pan-y 启用单指垂直平移手势

总结

核心代码加上注释一共217行,我们实现了一个可以在 PCMobile 上通用,并且可以无限嵌套的轮播组件
不到200行用Vue实现类似Swiper.js的轮播组件

结束

以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的