前言
大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等。
本文将详细讲解如何用Vue一步步实现的类似Swiper.js的功能,无任何第三方依赖,干货满满。
最终效果
在线预览:zyronon.github.io/douyin/
项目源代码:github.com/zyronon/dou…
注意:PC
必须将浏览器切到手机模式,先按 F12
调出控制台,再按 Ctrl+Shift+M
才能正常预览
Demo代码
上面的预览地址是最终实现的效果,下面才是本文代码实现的效果
为提升阅读体验,正文中代码展示有部分省略处理,完整代码可以在codesandbox上查看:
codesandbox.io/p/devbox/mu…
实现原理
布局
我们需要用到两个div,父元素 slide
设置 overflow: hidden 禁止滚动,子元素 slide-list
使用 flex 布局,然后将需要滚动的页面做为孙元素放在子元素 slide-list
中,由于子元素 slide-list
是 flex
布局,页面会自然的平铺排列
因为父元素 slide
的overflow: hidden
属性会将内容裁减,不提供滚动条,也不允许用户滚动,所以我们只能看到父元素 slide
宽高的内容。
1 | html复制代码<div class="slide"> |
1 | html复制代码.slide { |
滑动
实现滚动的关键点在于CSS3的 transform: translate(0, 0) 属性。
translate()
这个 CSS 函数在水平和/或垂直方向上重新定位元素,它的坐标定义了元素在每个方向上移动了多少。
因为子元素 slide-list
的内容是平铺的,我们只需要在子元素 slide-list
监听对应的事件,计算滑动的距离x
或y
,再动态设置到子元素 slide-list
的transform: translate(x, y)
里面,就可以实现页面滑动了
总结
大家可以将整个流程理解为播放胶片电影:父元素 A
是放映机,子元素 B
是胶片,而页面
是印刷在胶片上的内容。胶片每移动一格,我们就能看到新的一帧电影
实现
监听事件
PC 上的点击、移动,H5 的手势操作,都离不开 DOM 事件监听。例如鼠标移动事件对应 mousemove
,移动端因为没有鼠标则对应 touchmove
我们可以通过 Pointer 事件进行多端统一的事件监听,实现触屏和 PC 端通用
1 | html复制代码<div class="slide horizontal"> |
初始化
组件默认变量
1 | js复制代码//slide-list的ref引用 |
1 | js复制代码function slidePointerDown(e, el, state) { |
虽然我们用 Pointer事件
统一了移动端和PC端的监听事件,但 pointermove
事件在 PC
和移动端表现出来的效果却不一样,在 PC
上, pointermove
事件和 mousemove
事件一致,只要鼠标在目标元素上方,就会触发。而在移动端上却只有按下并移动时发才会触发
所以这里用一个 isDown
的变量保存是否按下的状态,pointermove
事件虽然会一直触发,但仅当 isDown
时才执行我们的代码逻辑
移动过程
1 | js复制代码function slidePointerMove(e,el,state) { |
用鼠标当前的位置,再减去鼠标按下时的位置,就是鼠标移动的距离
移动距离再加上当前页面 * 每个页面的宽或高,即子元素 slide-list
整体要偏移的量
技术难点
1. 如何判断滑动方向?是在上下滑还是左右滑?
1 | js复制代码//检测在对应方向上能否允许滑动,比如SlideHorizontal组件就只处理左右滑动事件,SlideVertical |
放大移动距离后再相除,根据结果是否大于1判断出滑动方向
2. 如何处理嵌套组件中的事件冲突?什么时候拦截事件和放行事件?
由于事件的冒泡机制,事件是从最里面的元素一级一级的往上冒泡的,所以我们只需在满足下面两个条件时拦截事件即可
- 是否在往到头或尾滑动
如果在第一页,不能往左/上滑动
如果在最后一面, 不能往右/下滑动
1 | js复制代码function canNext(state, isNext) { |
- 滑动方向和组件类型相匹配
SlideHorizontal.vue
组件只允许向左/右滑动SlideVertical.vue
组件只允许向上/下滑动
满足上述两个条件时拦截事件,不满足放行事件,交给上一级组件处理
1 | js复制代码//检测在对应方向上能否允许滑动 |
结束滑动
1 | js复制代码function slidePointerUp(e, state) { |
技术难点
- 如何让滑动结束时的动画更丝滑?
结合滑动时间、滑动距离来判断滑动下一条还是保持当前条
1、距离太短,直接不通过
2、距离太长,直接通过
3、若不在上述两种情况,那么只需要判断时间即可,小于150毫秒以内就算是成功滑动
其他问题
PC
上滑动有图片的页面,图片“分叉”了:我们开始拖动它的“克隆”
这是因为浏览器有自己的对图片和一些其他元素的拖放处理。它会在我们进行拖放操作时自动运行,并与我们的拖放处理产生了冲突
禁用它:
1 | js复制代码@dragstart="(e) => Utils.$stopPropagation(e)" |
PC
上滑动结束后触发了click事件
问题分析
首先我们滑动是利用 pointerdown
, pointermove
, pointerup
三个事件组合形成的,但是 pointerup
执行之后, click
是一定会执行的,是无法避免的,是无法用preventDefault
, stopPropagation
, stopImmediatePropagation
阻止的, 因为pointer
事件和 click
事件本身就不是一个系列的,因此没有关系,所以当发生滑动之后,pointerup
一定会执行,click
也会在 pointerup
执行后执行
解决方案
我们设置一个全局变量
1 | js复制代码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
的状态
1 | js复制代码window.isMoved = false |
设置了 overflow: auto
的页面在移动端不触发 pointermove
事件
再设置一个 touch-action:pan-y
就正常了
CSS 属性 touch-action
用于设置触摸屏用户如何操纵元素的区域 (例如,浏览器内置的缩放功能), pan-y
启用单指垂直平移手势
总结
核心代码加上注释一共217行,我们实现了一个可以在 PC
和 Mobile
上通用,并且可以无限嵌套的轮播组件
结束
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号
前端张余让
,我会更新更多实用的前端知识与技巧,期待与你共同成长~
本文转载自: 掘金