概述
这个组件的效果类似于 fullPage.js,通过鼠标滚轮或者上下滑动手势进行换页,每一页均是全屏的,在换页过程中带有过渡效果。
实现
1 | <div class="scroll-pane" v-scroll="handleScroll" v-touch:up="nextPage" v-touch:down="prevPage"> |
1 | import scroll from '@/directives/scroll' |
1 | .scroll-pane { |
解析
结构
HTML结构主要由三个部分组成,scroll-pane 是外框架,他是全屏的,并且不显示范围外的内容。
scroll-pane__wrapper 是页面容器,他和 scroll-pane 一样是全屏的,并且本身是一个 flex 容器,因为要实现的是垂直方向的换页,所以flex的方向是column。
scroll-pane__item 是页面元素,也需要要全屏显示。
关键的来了,由于wrapper没有设置overflow: hidden
,所以wrapper之中的内容是可以超过wrapper本身高度的,默认情况下flex布局的项目会自己进行缩放,然后向顶部看齐,所以需要flex-shrink: 0
限制项目的大小,让项目在位置不够时也不缩小。
wrapper 虽然是全屏的,但是其中的内容(item)早就已经超出他的范围了,这个时候根元素的overflow: hidden
就能隐藏超出范围的部分,相当于一张长纸条,用一个框把其中一块展示出来,框外的不显示。
既然item是平铺在 wrapper 中的,那么显示哪一个 item 的内容就可以使用transform
进行调整了,translateY
负责将wrapper的内容进行Y方向的偏移,移出一个屏幕(100%),就能看到下一个页面的内容。
事件
对于上下切换的操作,在PC端可以监听mousewheel
事件,但是滚轮的操作是很频繁的,这样会导致页面在短时间内被多次切换,所以这里需要使用去抖函数,对短期内的频繁操作做限制:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16export const instantDebounce = (fn, t) => {
const delay = t || 500
let timer
return function () {
const args = arguments
if (timer) {
clearTimeout(timer)
}
if (!timer) {
fn.apply(this, args)
}
timer = setTimeout(() => {
timer = null
}, delay)
}
}
这个函数主要是通过闭包实现的,timer 储存了先前得到的 timeout id。
在第一次使用时 timer 为空,先执行callback,然后给 timer 一个 id,第二次执行的时候由于闭包的特性,保留了 timer 的状态,这个时候 timer 不为空,这时候有两种情况:
- 第二次操作在 delay 范围内,那么很明显,第二次属于“频繁操作”,所以会执行
clearTimeout
将先前的timer操作取消。 - 第二次操作在 delay 范围外,这个操作是“合法”的,那么在第二次操作执行前,第一次的
setTimeout
函数已经执行了,此时timer为空,回到了前文“第一次使用”的情况。
简单来说就是如果前后操作的时间间隔小于 delay,那么只会重复创建 setTimeout 任务并且清除任务,大于 delay 时才会触发callback。
对于移动端,需要监听touchstart
等事件,根据起始点和结束点的位置,判断用户的手势,这里使用了 vue-directive-touch 实现,这个操作不需要去抖,因为手势很难在短时间内多次触发。
可用性
为了使组件能容纳更多类型的“内容”,不能简单使用slot实现,因为页面的数量是不确定的,不能提供不定量的slot元素。
将内容全放在default slot中,那么$slots.default的值为列表,无法将每一个列表元素单独使用scroll-page__item包裹(slot带来的元素会堆在一起)。
所以我选择了props作为页面元素的容器,在pages中能存放现有组件,并且能将组件通过component
元素渲染出来。