解析移动端滚动穿透

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

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


什么是滚动穿透

移动端开发中避免不了会在页面上进行弹窗、加浮层等这种操作。一个最常见的场景就是整个页面上有一个遮罩层,上面画着各种各样的东西,具体是什么就不讨论。实现这样一个遮罩层可难不住即使是一个刚开始写前端的小白。但是这里有一个问题就是如果不对遮罩层做任何处理,当用户在上面滑动时会发现遮罩层下方的页面居然也在滚动,这就很 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://www.fly63.com/article/detial/5195

BetterScroll移动端滚动场景的应用

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

css隐藏滚动条_基于webkit内容隐藏滚动条的方法总汇

基于webkit内容隐藏滚动条,核心代码是:overflow的值需要设置为auto或者scroll,然后设置::-webkit-scrollbar的display属性为none。当然也可以通过设置滚动条的宽度为0。

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

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

基于iScroll实现内容滚动

iScroll 是一款针对web app使用的滚动控件,它可以模拟原生IOS应用里的滚动列表操作,还可以实现缩放、拉动刷新、精确捕捉元素、自定义滚动条等功能。

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

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

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

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

无间歇文字滚动_ 原生js实现新闻无间歇性上下滚动

这篇文章主要介绍使用js实现文字无间歇性上下滚动,一些网站的公告,新闻列表使用的比较多,感兴趣的小伙伴们可以参考一下

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

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

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

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

使用CSS实现无滚动条滚动

我们都知道,撸页面的时候当我们的内容超出了我们的div,往往会出现滚动条,影响美观。百度下大部分都是在说overflow:hidden或者overflow-y: no可以解决问题,但是并不能很好的解决我们的问题,那么怎么办呢?

点击更多...

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