解析移动端滚动穿透

更新日期: 2019-09-08 阅读: 3.9k 标签: 滚动

滚动穿透在移动端开发中是一个很常见的问题,产生诡异的交互行为,影响用户体验,同时也让我们的产品看起来不那么“专业”。虽然不少产品选择容忍了这样的行为,但是作为追求极致的工程师,应该去了解为什么会产生以及如何去解决。


什么是滚动穿透

移动端开发中避免不了会在页面上进行弹窗、加浮层等这种操作。一个最常见的场景就是整个页面上有一个遮罩层,上面画着各种各样的东西,具体是什么就不讨论。实现这样一个遮罩层可难不住即使是一个刚开始写前端的小白。但是这里有一个问题就是如果不对遮罩层做任何处理,当用户在上面滑动时会发现遮罩层下方的页面居然也在滚动,这就很 interesting 了。就如下面的例子,一个名为mask长宽都是屏幕大小的遮罩层,我们在上面滑动时,下面的内容也在跟随滚动,即滚动“穿透”到了下方,这就是滚动穿透(scroll-chaining)。

scroll-chaining

上方 demo 的遮罩层底部是一个逐渐变蓝的内容容器,但是滑动上面遮罩层时,底部也跟随滚动了,这只是一个最简单的场景,后面我们会讨论更复杂的情况。


为什么会出现

目前 Google 上搜滚动穿透会出现一大堆教你如何解决的文章,但是它们都是在告诉你怎么解决怎么 hack 掉这种交互异常。并没有告诉读者为什么会产生这种行为,甚至认为这是浏览器的一个 bug。对于我来说这个是难以理解的,因为就算解决了问题,其实也并不知道问题的根本是怎样的。


认知误区

有一个误区就是我们设置了一个和屏幕一样大小的遮罩层,盖住了下面的内容,按理说我们应该能屏蔽掉下方的所有事件也就是说不可能触发下面内容的滚动。那么我们就去看一下规范,什么时候会触发滚动。

// https://www.w3.org/TR/2016/WD-cssom-view-1-20160317/#scrolling-events
When asked to run the scroll steps for a Document doc, run these steps:

  1. For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:
  • If target is a Document, fire an event named scroll that bubbles at target.
  • Otherwise, fire an event named scroll at target.
  1. Empty doc’s pending scroll event targets.

通过规范我们可以明白的 2 点是,首先滚动的 target 可以是 document 和里面的 element。其次,在 element 上的 scroll 事件是不冒泡的,document 上的 scroll 事件冒泡。

所以如果我们想通过在 scroll 的节点上去阻止它的滚动事件冒泡来解决问题是不可行的!因为它根本就冒泡,无法触及 dom tree 的父节点何谈触发它们的滚动。

那么问题是怎么产生的呢,其实规范只说明了浏览器应该在什么时候滚动,而没有说不应该在什么时候滚动。浏览器正确实现了规范,滚动穿透也并不是浏览器的 bug。我们在页面上加了一个遮罩层并不会影响 document 滚动事件的产生。根据规范,如果目标节点是不能滚动的那么将会尝试 document 上的滚动,也就是说遮罩层虽然不可滚动,但是这个时候浏览器会去触发 document 的滚动从而导致了下方文档的滚动。也就是说如果 document 也不可滚动了,也就不会有这个问题了。这就引出了解决问题的第一种方案:把 document 设置为 overflow hidden。


怎么解决

overflow hidden

既然滚动是由于文档超出了一屏产生的,那么就让它超出部分 hidden 掉就好了,所以在遮罩层被弹出的时候可以给 html 和 body 标签设置一个 class:

.modal--open {
  height: 100%;
  overflow: hidden;
}

这样文档高度和屏幕一样,自然不会存在滚动了。但是这样又会引来一个新的问题,如果文档之前存在一定的滚动高度那么这样设置后会导致之前的滚动距离失效,文档滚回了最顶部,这样一来岂不是得不偿失?但是我们可以在加 class 之前记录好之前的滚动具体然后在关闭遮罩层的时候把滚动距离设置回来。这样问题是可以得到解决的实现成本也很低,但是如果遮罩层是透明的,弹出后用户仍然会看到丢失距离后的下方页面,显然这样并不是完美的方案。

prevent touch event

还有一种办法就是我们直接阻止掉遮罩层和弹窗的 touch event 这样就不会在移动端触发 scroll 事件了。但是在 PC 上没有 touch 事件, scroll 事件仍然可以被触发,原因上面我们也说过,scroll 事件是滚动它能滚动的元素。这里我们解决的是移动端的问题,例子如下:

scroll-chaining

<div id="app">
  <div class="mask">mask</div>
  <div class="dialog">dialog</div>
</div>
const $mask = document.querySelector(".mask");
const $dialog = document.querySelector(".dialog");
const preventTouchMove = $el => {
  $el.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    { passive: false }
  );
};
preventTouchMove($mask);
preventTouchMove($dialog);

上面我们通过 prevent touchmove 来阻止页面的触摸事件从而禁止进一步的页面滚动,在 addEventListener 最后一个参数我们将 passive 显示的设置为 false,这里是有用意的。关于 passive event listener 这里又是一个话题我们就不展开说了,就是浏览器为了优化滚动性能做的一些改进,具体可以看 网站使用被动事件侦听器以提升滚动性能,由于在 Chrome 56 开始将会默认开启 passive event listener 所以不能直接在 touch 事件中使用 preventDefault,需要先将 passive 选项设置为 false 才行。

这里我们解决了在页面上普通弹窗的问题,但是如果 dialog 的内容是可以滚动的,这样将其阻止了 touch 事件将会导致其内容也不能正常滚动,所以还有要进一步优化才行。


进一步优化

现在的场景是我们的弹窗是可以滚动的,所以不能再直接将其 touch 事件阻止,去掉后我们发现会产生新的问题。遮罩层被阻止了 touch 事件不能使下方滚动,但是弹出层 modal 这里内容是可滚动的,在 touch modal 时能正常滚动里面的内容。但是 modal 滚动到最上方或者最下方时仍然能触发 document 的滚动,效果如下:

scroll-chainging

我们看到当 modal 滚动在顶部时仍然能拖动下方 document。这样我们只能监听用户手势,如果 modal 已经滑动到了底部或者顶部且还要往上或者下滑动则也要 prevent modal 的 touch 事件。简单实现一个 fuckScrollChaining 函数

function fuckScrollChaining($mask, $modal) {
  const listenerOpts = { passive: false };
  $mask.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    listenerOpts
  );
  const modalHeight = $modal.clientHeight;
  const modalScrollHeight = $modal.scrollHeight;
  let startY = 0;

  $modal.addEventListener("touchstart", e => {
    startY = e.touches[0].pageY;
  });
  $modal.addEventListener(
    "touchmove",
    e => {
      let endY = e.touches[0].pageY;
      let delta = endY - startY;

      if (
        ($modal.scrollTop === 0 && delta > 0) ||
        ($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)
      ) {
        e.preventDefault();
      }
    },
    listenerOpts
  );
}

完整实现在 这里,至此无论弹出层内容是否可滚动都不会导致下方 document 跟随滚动。

原文来自:https://github.com/Jiavan/blog/issues/2

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/5195

相关推荐

js禁止页面滚动

发移动端页面的时候有一个很比较常见的需求,在出现弹窗时,禁止滑动弹窗后面的主体页面。如何实现呢,往下看

js如何实现滚动到一定位置将内容固定在页面顶部

window.onscroll为滚轮监听,document.body.scrollTop||document.documentElement.scrollTop 写两个是为了兼容不同浏览器。固定位置的top要设为负值,原因不明,若为0则会跟上方有空隙。左右位置虽然是0也要设,不然若为不是100%宽度的内容会出现左右跳动。

js 禁止/允许页面滚动

passive,设置该属性的目的主要是为了在阻止事件默认行为导致的卡顿。等待监听器的执行是耗时的,有些甚至耗时很明显,这样就会导致页面卡顿。即便监听器是个空函数,也会产生一定的卡顿,毕竟空函数的执行也会耗时

js实现返回顶部效果的方法总结

当页面特别长的时候,用户想回到页面顶部,必须得滚动好几次滚动键才能回到顶部,如果在页面右下角有个返回顶部的按钮,用户点击一下,就可以回到顶部,对于用户来说,是一个比较好的体验。

js+css 实现 滚动条滑动时显示,不滑动时隐藏

把原有的滚动条隐藏,创建个新的滚动条,并拓展其宽度,达到隐藏的效果,而判断滚动条是否滚动,保存原有的滚动条到顶部的距离,看是否发生改变,做出相应的判断。

使用elementUI滚动条之横向滚动

这里要简单的设置一下,将标签的height设为100%,读者查看效果的时候,会出现一个横向的滚动条,如果你不想要横向的滚动条,使用下面css属性设置就可以只显示竖向滚动条。

BetterScroll移动端滚动场景的应用

BetterScroll 是一款重点解决移动端各种滚动场景需求的开源插件,有下列功能支持滚动列表,下拉刷新,上拉刷新,轮播图,slider等功能。better-scroll通过使用惯性滚动、边界回弹、滚动条淡入淡出来确保滚动的流畅。

原生js获取浏览器获X轴,Y轴的滚动距离

在前端开发中,需要获取浏览器滚动距离的需求,这篇文章主要讲解如何使用原生Js兼容实现获取浏览器获X轴,Y轴的滚动距离。并延伸扩展下我们一些不知道的js知识,希望对你有所帮助。

CSS让页面平滑滚动

在PC浏览器中,网页默认滚动是在<html>标签上的,移动端大多数在<body>标签上,业界浏览器的CSS reset都可以加上这么一条规则:凡是需要滚动的地方都加一句scroll-behavior:smooth就好了!

原生js判断加载更多事件,通过获取页面滚动距离、文档总高度、浏览器视口高度

原生js获取滚动条在Y轴上的滚动距离、获取文档的总高度、获取浏览器视口的高度的方法实现,用于判断页面加载数据。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!