背景
-
需求概述: 在【裂变迁移任务系统3.0】需求中,PM和QA提出半屏弹层需要处理任务条目大于4条的情况,即支持滚动。此外还要求半屏弹层打开时背景页暂时关闭滚动。
-
先前解决方案: 任务系统3.0中弹窗组件的统一处理是调用useBodyTouchMovePreventDefault方法。当弹窗组件打开时,全局禁用document.body的touchmove事件,关闭时再重新打开。此方法并不能直接套用至此需求的新组件,主要原因是目前弹窗组件中的内容是默认不需要滚动的。若我们全局禁止了touchmove事件,那很多后续的处理都是失效的,例如给半屏弹层组件添加overflow: scroll属性,这并不能使弹层内容滚动。
问题常见场景
2.1 遮罩层滚动穿透
(1) 当弹层内容需做滚动但背景页也具有滚动能力时, 经常会出现下面这种情况: 在我们滑动遮罩层或者非弹层可滑动内容区域时, 背景页也跟着一起滚动。
2.2 弹层内容滚动穿透
(2) 当弹层可滑动内容滑至底部/顶部时, 背景页开始滚动。
解决方案
3.1 使用overflow: hidden
第一种解决方案是在弹层组件中执行以下图中的代码,在组件加载的时候将body主体超过屏幕的部分进行隐藏,且设置整体的height刚好为100vh,起到关闭滚动条的作用。二者相结合就能在不影响弹层滑动能力的同时禁止背景页滑动。除此之外,还需要在组件卸载时及时还原body原始的设置。这里有个要点值得一提。首先是在某些场景下,当背景页的整体高度超过一屏幕且已经滚动了一部分距离,这时候我们关闭弹层,背景页就会自动滚回到顶部并停留,这种体验对用户肯定是不友好的。所以我们可以在弹层组件打开时记下body此时的滚动高度,在关闭时给body的scrollTop赋值,就可以预防背景页回到顶部的情况发生。
export const useActionSheetTouchMovePreventDefault = () => {
let scrollTop = 0;
onMounted(() => {
// 记住当前的滚动位置
scrollTop = document.body.scrollTop;
// 防止首页滚动
document.body.style.overflow = 'hidden';
// 防止首页在滚动被禁用时产生滚动条
document.body.style.height = '100vh';
});
onUnmounted(() => {
// 把当前的滚动位置设置为先前的scrollTop, 防止首页自动回到顶部
document.body.scrollTop = scrollTop;
document.body.style.overflow = 'auto';
document.body.style.height = 'auto';
});
};
3.2 局部禁止touchmove事件
第二种能想到的比较简单的方法是直接对弹层非滑动区域暂时禁止touchmove事件。这个方法比较适合非滑动区域比较单一的情况,弹层的组成部分越简洁越好。但是对应本次需求的半屏弹层非滑动部分除了最基本的遮罩层之外,还有额外的修饰图片和文字部分也是要做禁止touchmove处理的,这意味着一次性要获取多个dom元素且都要添加和卸载addEventListener事件。另一方面,有些dom元素还要通过扩充真实宽度使其占满一行来更好的绑定监听事件,例如使用伪元素等。综合这些方面考虑,此方法不是很理想。
$('.mask').addEventListener('touchmove', (e: TouchEvent) => {
e.preventDefault();
})
除了上面的问题,使用此方法通常还会出现问题常见场景的第二种情况。在内容区域滚动的时候,只要没有到达顶部或者底部的时候,滚动内容区域背景是不会跟着滚动的,但是当我们滚动到底部的时候继续向下滑动,背景页就发生了滚动。所以这时候我们要做额外处理去阻止其默认行为。
$('.content').addEventListener('touchstart', function (e: TouchEvent) {
const targetTouches = e.targetTouches || []
if (targetTouches.length > 0) {
const touch = targetTouches[0] || {};
// 手指距离顶部的距离
startY = touch.clientY;
}
})
$('.content').addEventListener('touchmove', function(e: TouchEvent) {
const targetTouches = e.targetTouches;
const scrollTop = content.scrollTop;
if (targetTouches.length > 0) {
const touch = targetTouches[0] || {};
// 手相对屏幕的一个位置
const moveY = touch.clientY;
// 滚动条滑动至顶部 且 正在向上滑
if ((scrollTop === 0 && moveY > startY) ||
// 滚动条滑动至底部 且 正在往下滑
(scrollTop === scrollHeight - offsetHeight && moveY < startY)
) {
e.preventDefault();
}
}
})
总结
第三小节提供了两种不同的方法,两种方法都有各自的优缺点。
-
使用overflow: hidden方法的通用性更强,代码也更简洁。但是也有一些问题,例如此方法会触发回流影响页面性能,且某些场景下背景页会自动回到顶部。
-
使用局部禁止touchmove事件的用户体验会更好一些,但是对于dom结构比较复杂的弹层容器来说,需要给多个dom添加事件监听器。此外还需要处理滑动至底部/顶部时,背景页跟着滑动的特殊情况。