前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【兼容性】H5滚动穿透解决方案

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

作者头像
神仙朱
发布2021-12-29 15:40:32
5K0
发布2021-12-29 15:40:32
举报

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

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

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

不废话,本文分为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

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神仙朱 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档