当前位置: 首页 > Web前端 > CSS

移动端滚动渗透分析

时间:2023-03-31 00:34:57 CSS

滚动渗透是移动端开发中非常普遍的问题,会产生怪异的交互行为,影响用户体验,也让我们的产品看起来不够“专业”。虽然很多产品选择容忍这样的行为,但作为一个追求极致的工程师,他应该明白为什么会发生,以及如何解决。什么是滚动?在移动端的开发中,不可避免地要对页面进行弹窗、浮层等操作。最常见的一种场景就是整个页面上有一个遮罩层,上面画了各种东西,具体的就不说了。实现这样的遮罩层,即使对于刚开始写前端的新手来说,也很难实现。但是这里有一个问题,如果不对遮罩层做任何处理,当用户在上面滑动的时候,会发现遮罩层下面的页面居然在滚动,这个很有意思。就像下面的例子,一个名为mask的遮罩层,它的长宽都是屏幕的大小,当我们在上面滑动的时候,下面的内容也在滚动,也就是滚动“穿透”到底部,也就是滚动渗透(滚动链接)。上面的demo中遮罩层的底部是一个逐渐变成蓝色的内容容器,但是当滑动上面的遮罩层时,底部也会滚动。这只是最简单的场景,以后再讨论更复杂的情况。为什么在谷歌搜索滚动穿透时出现很多教你如何解决的文章,但都是在告诉你如何解决,如何hack这种交互异常。它没有告诉读者为什么会出现这种行为,甚至认为这是一个浏览器错误。这让我很难理解,因为即使解决了问题,其实我也不知道问题的根源是什么。认知误区一个误区是我们设置了一个和屏幕一样大小的遮罩层来遮住下面的内容。按理说我们应该可以屏蔽下面的所有事件,也就是说下面的内容的滚动是不可能触发的。那么我们来看看规范,什么时候触发滚动。//https://www.w3.org/TR/2016/WD...当要求运行文档文档的滚动步骤时,运行这些步骤:对于文档的待定滚动事件目标中的每个项目目标,按顺序它们被添加到列表中,运行这些子步骤:如果目标是文档,则触发一个名为scroll的事件,该事件在目标处冒泡。否则,在目标处触发一个名为scroll的事件。空文档的未决滚动事件目标。通过规范我们可以了解到第二点,第一个滚动目标可以是文档和里面的元素。其次,element上的scroll事件没有冒泡,document上的scroll事件是冒泡的。所以如果我们想通过阻止它的滚动事件在滚动节点上冒泡来解决问题,是行不通的!因为它根本不冒泡,所以无法触及dom树的父节点来触发它们的滚动。那么问题是如何产生的呢?事实上,规范只规定了浏览器什么时候应该滚动,而没有规定什么时候不应该滚动。浏览器正确地实现了规范,滚动浏览不是浏览器错误。我们在页面上添加了一个遮罩层,它不会影响文档滚动事件的产生。根据规范,如果目标节点无法滚动,它会尝试在文档上滚动。也就是说虽然遮罩层不能滚动,但是此时浏览器会触发文档的滚动,导致下面的文档滚动。也就是说,如果文档是不可滚动的,就不会有这个问题。这就引出了第一个解决问题的方法:设置文档溢出隐藏。overflowhidden如何解决由于滚动是文档超出一屏导致的,所以让它超出隐藏部分就好了,所以在弹出mask层的时候,可以给html和body标签设置一个class:。modal--open{height:100%;overflow:hidden;}这样,文档的高度就和屏幕一样高了,自然就不会出现滚动了。但这会导致新的问题。如果文档之前有一定的滚动高度,那么这个设置会导致之前的滚动距离失效,文档会回滚到顶部。得不偿失吗?但是我们可以在添加类之前记录之前的滚动细节,然后在遮罩层关闭时将滚动距离设置回来。这个问题是可以解决的,实现成本很低,但是如果遮罩层是透明的,弹出后失去距离,用户还是会看到下页,显然不是一个完美的解决方案。另外一种防止触摸事件的方法是我们直接防止遮罩层和弹窗的触摸事件,这样在移动端就不会触发滚动事件。但是PC端没有touch事件,scroll事件还是可以触发的。上面我们也说了原因,scroll事件就是滚动它可以滚动的元素。这里我们解决的是移动端的问题,示例如下:mask

dialog
const$mask=document.querySelector(".mask");const$dialog=document.querySelector(".dialog");constpreventTouchMove=$el=>{$el.addEventListener("touchmove",e=>{e.preventDefault();},{passive:false});};preventTouchMove($mask);preventTouchMove($dialog);上面我们使用preventtouchmove来阻止页面的touch事件来禁止进一步的页面滚动,最后一个在addEventListener参数中我们设置了passivedisplay为false,这里是有意的。关于passiveeventlistener这里是另外一个话题,我们就不展开了。它是浏览器为优化滚动性能所做的一些改进。有关详细信息,您可以看到网站使用被动事件侦听器来提高滚动性能。由于Chrome56启动时会默认启用passive事件监听器,所以不能直接在touch事件中使用preventDefault,需要先将passive选项设置为false。这里我们已经解决了页面上普通弹窗的问题,但是如果dialog的内容可以滚动的话,屏蔽touch事件会导致内容不能正常滚动,所以还需要进一步优化。为了进一步优化当前场景,我们的弹窗是可以滚动的,所以我们不能再直接屏蔽它的触摸事件了。去掉之后,我们发现又会出现新的问题。屏蔽层被遮挡,触摸事件不能使底部滚动,但是弹出层modal的内容是可以滚动的,触摸modal时里面的内容可以正常滚动。但是,当模态滚动到顶部或底部时,仍然可以触发文档的滚动。效果如下:我们可以看到当modal滚动到顶部的时候,下面的document还是可以拖动的。这样,我们只能监测用户的手势。如果modal已经滑到底部或顶部,还在向上或向下滑动,还需要preventmodal的touch事件。简单地实现一个fuckScrollChaining函数:functionfuckScrollChaining($mask,$modal){constlistenerOpts={passive:false};$mask.addEventListener("touchmove",e=>{e.preventDefault();},listenerOpts);constmodalHeight=$modal.clientHeight;constmodalScrollHeight=$modal.scrollHeight;让startY=0;$modal.addEventListener("touchstart",e=>{startY=e.touches[0].pageY;});$模态。addEventListener("touchmove",e=>{letendY=e.touches[0].pageY;letdelta=endY-startY;if(($modal.scrollTop===0&&delta>0)||($modal.scrollTop+modalHeight===modalScrollHeight&&delta<0)){e.preventDefault();}},listenerOpts);}完整的实现到此为止,至此无论弹出层内容是否滚动,下面的文档不会跟随滚动。原文出处欢迎讨论https://github.com/Jiavan/blo...