【兼容性】H5滚动穿透解决方案

2021-12-29 15:40:32 浏览数 (1)

小东西快快学快快记,大知识按计划学,不拖延

滚动穿透相信大家平常开发的时候也经常遇到,网上也有很多解决办法

今天我就谈下我对 滚动穿透的理解 和 总结下我们大佬写的一个比较完美的解决方案

不废话,本文分为3部分

1、什么是滚动穿透

2、为什么会滚动穿透

3、怎么解决滚动穿透

4、碰到的问题

什么是滚动穿透

大家肯定不陌生了,做移动端开发的,肯定都碰到过,比如 我明明滚动的是弹窗,但是底下的 document 却在滚动

不说这么多,直接看

为什么会滚动穿透

首先,这不是一个bug,这是一个合理且正常的表现

阅读了官方的文档之后,我也是理解了好久

https://www.w3.org/TR/cssom-view/#scrolling

以下是个人的理解

当用户开始滚动的时候,页面响应滚动有两种类型

1、document 滚动

2、可滚动 element 滚动

只有两种类型,就是说,一旦有滚动行为发生,那么就必然产生这两个类型其中之一

如果 element 可以滚动,那么就 滚动 element

如果 element 无法滚动,那么就让 document 响应滚动

是一个 if-else 的关系

这个element 无法滚动包括

  1. 没有设置可滚动overflow属性
  2. 监听回调 设置了 preventDefault
  3. 已经滚动到底端或顶端

为什么会觉得这个这个行为是合理性,我的理解是

用户产生滚动行为,浏览器就必须要响应这个行为,产生滚动的反馈,这才是正常的。尽可能响应,滚动一切当前操作可以滚动的元素

只是当把元素设置了 fixed 之后让人感觉是个bug,浏览器没有必要对 fixed 元素做特殊处理,两个不相关的东西,不可能耦合起来

怎么解决滚动穿透

我们理解了滚动穿透的原因之后,我们就可以对症下药了

既然 document 是备胎滚动选项,那么就让 document 不可滚动

1body overflow hidden

代码语言:javascript复制
html, body { overflow: hidden; }

PC 可以,但是对移动端无效

那么我们限制body不超过一屏,那么自然就不能滚动了?

2body height 100%

代码语言:javascript复制
html, body { overflow: hidden; height:100%}

是可以,但是会丢失 滚动高度,文档回到最顶部。体验不好

3记录滚动高度,弹窗关闭重新赋值

既然丢失滚动高度,那么就记录下滚动高度 scrollTop ?然后关闭弹窗的时候再赋值回去?

页面内容从 0 突然跳到 原先位置,可想而知会有 闪动,体验仍然不好

4避免页面跳回顶部

拿到 页面的滚动高度,在给 html 设置 这些样式的时候

代码语言:javascript复制
html{ overflow: hidden; height:100%}

在设置 absolute,top 设置成之前拿到滚动高度(伪代码)

代码语言:javascript复制
html {
    position:absolute;
    top: scrollTop
}

利用这种方式保证内容处在同一位置,这样就可以避免页面的跳动,但是直接给 html 设置 absolute 风险太大,容易埋坑,不太建议大项目使用,小应用还是可以的,我在需求的小活动页7就使用过这种方式

5禁用页面滚动

除了在 css 限制页面滚动,还可以从 js 去限制

代码语言:javascript复制
document.addEventListener( 'touchmove', e => e.preventDefault());

这里要注意一个问题,在 chrome51 中在监听回调更新了参数,如果你不加上这个参数,那么可能这样并不能禁用页面滚动

具体如下

以前 addEventlisener 参数 是

代码语言:javascript复制
target.addEventListener(type, listener[, useCapture]);

第三个参数是 控制监听器是 捕获阶段还是 冒泡阶段执行,默认值是 false(冒泡阶段执行)

现在变成了

代码语言:javascript复制
target.addEventListener(type, listener[, options]);

第三个参数变成了对象,包含一个属性 passive

这个参数主要是为了提高滚动流畅度

因为在一开始的时候,浏览器响应滚动 大概会有 200ms 的延迟

因为浏览器不知道监听的回调是否调用了 preventDefault 来取消滚动

所以只好等回调执行完毕,大概 200ms 后, 页面再开始响应滚动,所以会显得不那么跟手

现在通过 参数 passive 就可以事先告诉浏览器 这个监听回调不会 执行 preventDefault,你可以马上响应滚动不用等待

从而 提升了滚动的流畅度

但是 passive 是新出的标准,但是以前没有,所以我们需要做一个兼容

代码语言:javascript复制
var options = false;
window.addEventListener("test", null, {
  get passive() {
    options = { passive: true };
    return undefined;
  },
});
elem.addEventListener("touchstart", fn, options);

具体可以看下 justjavac 写的文章

https://zhuanlan.zhihu.com/p/24555031

所以我们禁用页面滚动,可能得这么写,告诉浏览器我们需要禁用滚动

代码语言:javascript复制
document.addEventListener(
  'touchstart', 
  e => e.preventDefault(), 
  { passive: false}
);

但是这样就会把页面所有滚动都禁止

所以我们需要开放一个白名单,当滚动的元素在白名单之内,我们就放开限制

这个白名单的设置就是 给元素加上 can-scroll 类名,这样就可以放开滚动

代码语言:javascript复制
document.addEventListener(
  "touchmove",
  (e) => {
    const excludeEl = document.querySelectorAll(".can-scroll");
    const isExclude = [].some.call(excludeEl, (el: HTMLElement) =>
      el.contains(e.target)
    );
    if (isExclude) {
      return true;
    }
    e.preventDefault();
  },
  { passive: false }
);

但是对待白名单的元素放开限制之后,当元素滚动到顶部和底部的时候,再滚动,仍然会触发document 滚动

为什么呢?

之前我们说了,浏览器需要尽可能响应滚动行为,element 滚到两端 element 滚不了,那我就滚 document

所以我们最好监听 element 滚到 顶部和 底部的时机,继续禁止滚动行为

代码语言:javascript复制
var initialY = 0;
el.ontouchstart = function (e) {
  if (e.targetTouches.length === 1) {
    // 单点滑动
    initialY = e.targetTouches[0].clientY;
  }
};
el.ontouchmove = function (e) {
  if (e.targetTouches.length === 1) {
    // 单点滑动
    var clientY = e.targetTouches[0].clientY - initialY;
    // 滑到底部
    if (el.scrollTop   el.clientHeight >= el.scrollHeight && clientY < 0) {
      return e.preventDefault();
    }
    // 滑到顶部
    if (el.scrollTop <= 0 && clientY > 0) {
      return e.preventDefault();
    }
  }
};

碰到的问题

1父子元素也存在滚动穿透

这个问题测试了,只在 ios 中存在,滚动穿透的顺序是 子->父->document,而 安卓和 鸿蒙 则不会,子滚不了,直接滚document

这个是实际的dom 父子关系才会,视觉上的 父子关系没有这个问题

2子元素 e.stopPropagation() 会让 preventDefault 失效

比如这样

代码语言:javascript复制
document.addEventListener(
  "touchmove",
  (e) => {
    e.preventDefault();
  },
  { passive: false }
);

document.querySelector(".modal").addEventListener("touchmove", (e) => {
  e.stopPropagation();
});

虽然document 取消了默认事件,本来整个页面都不能滚了

但是子元素 调用了 stopPropagation() 之后,不仅元素可以滚了,还会导致滚动穿透(毕竟只要元素能滚就能发生穿透)

但是document 还是不会滚动的

3滚动穿透的触发条件

一次没有抬起的滚动行为(手没有离开屏幕)导致元素滚动到顶部或者 底部之后,如果手还在屏幕上往两端滑,并不会触发滚动穿透

如果你把元素滚动到 两端不可滚之后,抬起手,再按下去,往不可滚的方向移动,此时才会发生 滚动穿透

之前我们说了,滚动响应有两种对象,element 和 document

从这里可以意识到,单次的滚动行为 只会绑定一个滚动对象,不会切换响应对象

只是在开始滚动的时候,浏览器会根据情况,选择响应滚动的对象,选择时候不会切换

0 人点赞