不到200行用Vue实现类似Swiper.js的轮播组件
前言
大家在开发过程中,或多或少都会用到轮播图之类的组件,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-list
是 flex
布局,页面会自然的平铺排列
因为父元素 slide
的overflow: hidden
属性会将内容裁减,不提供滚动条,也不允许用户滚动,所以我们只能看到父元素 slide
宽高的内容。
<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;
}
滑动
实现滚动的关键点在于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 端通用
<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. 如何处理嵌套组件中的事件冲突?什么时候拦截事件和放行事件?
由于事件的冒泡机制,事件是从最里面的元素一级一级的往上冒泡的,所以我们只需在满足下面两个条件时拦截事件即可
- 是否在往到头或尾滑动
如果在第一页,不能往左/上滑动
如果在最后一面, 不能往右/下滑动
function canNext(state, isNext) {
return !(
(state.localIndex === 0 && !isNext) ||
(state.localIndex === state.wrapper.childrenLength - 1 && isNext)
)
}
- 滑动方向和组件类型相匹配
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行,我们实现了一个可以在 PC
和 Mobile
上通用,并且可以无限嵌套的轮播组件
结束
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号,我会更新更多实用的前端知识与技巧,期待与你共同成长~
不到200行用Vue实现类似Swiper.js的轮播组件的更多相关文章
- 3d轮播图——类似酷狗的轮播
说到轮播图,其实只要是跟web开发相关的无论是前端后端应该都不陌生,各种各样的轮播图,从以前的单纯的平面山水画遮盖滑动或滚动,到Jquery的animate甚至是h5+css3,各种炫酷的轮播图更是层 ...
- vue中引入awesomeswiper的方法以及编写轮播组件
1.先安装less-loader npm install less less-loader --save 2.再安装css-loader npm install css-loader --save 3 ...
- vue-awesome-swipe 基于vue使用的轮播组件 使用(改)
npm install vue-awesome-swiper --save //基于vue使用的轮播组件 <template> <swiper :options="swi ...
- vue.js层叠轮播
最近写公司项目有涉及到轮播banner,一般的ui框架无法满足产品需求:所以自己写了一个层叠式轮播组件:现在分享给大家: 主要技术栈是vue.js ;javascript;jquery:确定实现思路因 ...
- vue 3d轮播组件 vue-carousel-3d
开发可视化项目时,需要3d轮播图,找来找去发现这个组件,引用简单,最后实现效果还不错.发现关于这个组件,能搜到的教程不多,就分享一下我的经验. 插件github地址:https://wlada.git ...
- Vue中实现数据列表无缝轮播
类似这种滚动轮播效果 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta char ...
- vue渲染数据后与owlCarousel轮播插件冲突,失效
主要原因:dom解析准备完成后,开始执行$(document).ready(); 而vue是在window.onload(页面加载完后才执行):所以会导致owlCarousel插件失效. 解决方案:数 ...
- vue轮播组件及去掉路由#
最近公司要我去实现vue知识的系统讲解,总结一番,大致需要7节课,今天大致说一下我们使用vue需要学会的基本技能.vue是一个渐进性视图渲染框架,使用vue核心是数据出发,数据一般是我们前台从后台获取 ...
- Vue学习—Vue写一个图片轮播组件
1.先看效果: 熟悉的图片轮播,只要是个网站,百分之90以上会有个图片轮播.我认为使用图片轮播. 第一可以给人以一种美观的感受,而不会显得网站那么呆板, 第二可以增加显示内容,同样的区域可以显示更多内 ...
- vue 写一个炫酷的轮播图
效果如上图: 原理: 1.利用css 的 transform 和一些其他的属性,先选五张将图片位置拍列好,剩余的隐藏 2.利用 js 动态切换类名,达到切换效果 css代码如下 .swiper-cer ...
随机推荐
- redis 简单整理——客户端案例分析[十八]
前言 简单整理一下客户端案例分析. 正文 现象一: 服务端现象:Redis主节点内存陡增,几乎用满maxmemory,而从节点 内存并没有变化. 客户端现象:客户端产生了OOM异常,也就是Redis主 ...
- Pytorch-均方差损失函数和交叉熵损失函数
均方差损失函数mse_loss()与交叉熵损失函数cross_entropy() 1.均方差损失函数mse_loss() 均方差损失函数是预测数据和原始数据对应点误差的平方和的均值. \[MSE=\f ...
- 《Effective C#》系列之(一)——异常处理与资源管理
请注意,<Effective C#>中的异常处理与资源管理部分实际上是第四章的内容.以下是关于该章节的详细解释. 第四章:异常处理与资源管理 一. 了解异常处理机制 异常处理机制使程序员能 ...
- 【vue】纯前端图形验证码实现
[vue]纯前端图形验证码实现 感觉人不能在SQL里面淹死,得看看别的东西了 因为是上班摸鱼偷摸搞的,所以人比较懒,很多东西也懒得修修改改,直接放在一个html文件下了 页面如下 js的生成图形逻辑是 ...
- 力扣627(MySQL)-变更性别(简单)
题目: Salary 表: 请你编写一个 SQL 查询来交换所有的 'f' 和 'm' (即,将所有 'f' 变为 'm' ,反之亦然),仅使用 单个 update 语句 ,且不产生中间临时表. 注意 ...
- 力扣238(java)-除自身以外数组的乘积(中等)
题目: 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 . 题目数据 保证 数组 nums之中任意元素的全 ...
- MaxCompute跨境访问加速解决方案
简介: MaxCompute联合全球加速服务,为有跨境访问需求的MaxCompute客户提供一套高效稳定的跨境访问加速方案. MaxCompute联合全球加速服务,为有跨境访问需求的MaxComput ...
- Gradle构建SpringBoot单模块项目
Gradle构建SpringBoot单模块项目 方式Ⅰ:未基于:Gradle Wrapper 方式Ⅱ:(推荐使用)Gradle Wrapper[可以不安装Gradle.统一Gradle的版本]--包括 ...
- SpringBoot配置两个一样的Bean,区分两个配置类——@Primary
1.@Primary 作用: 指定默认bean. 当没有根据名字显示要注入哪个bean的时候,默认使用打了@Primary标签的bean 2.配置两个一样的bean @Configuration pu ...
- ES Monitoring 整理笔记
Monitoring是elastic stack的监控模块,监控信息存在es索引中,并且可以通过kibana进行可视化的展示.(收集监控数据的方式从6.5版本起由Collectors-Exporter ...