前端滚动穿透问题技术探讨

背景

  • 需求概述: 在【裂变迁移任务系统3.0】需求中,PM和QA提出半屏弹层需要处理任务条目大于4条的情况,即支持滚动。此外还要求半屏弹层打开时背景页暂时关闭滚动。

  • 先前解决方案: 任务系统3.0中弹窗组件的统一处理是调用useBodyTouchMovePreventDefault方法。当弹窗组件打开时,全局禁用document.body的touchmove事件,关闭时再重新打开。此方法并不能直接套用至此需求的新组件,主要原因是目前弹窗组件中的内容是默认不需要滚动的。若我们全局禁止了touchmove事件,那很多后续的处理都是失效的,例如给半屏弹层组件添加overflow: scroll属性,这并不能使弹层内容滚动。

问题常见场景

2.1 遮罩层滚动穿透

(1) 当弹层内容需做滚动但背景页也具有滚动能力时, 经常会出现下面这种情况: 在我们滑动遮罩层或者非弹层可滑动内容区域时, 背景页也跟着一起滚动。

屏幕录制2023-09-12 11.17.14.mov

2.2 弹层内容滚动穿透

(2) 当弹层可滑动内容滑至底部/顶部时, 背景页开始滚动。

屏幕录制2023-09-12 11.23.37.mov

解决方案

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添加事件监听器。此外还需要处理滑动至底部/顶部时,背景页跟着滑动的特殊情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值