滑动穿透终极解决方案

2023-11-09

问题描述

滑动穿透:浮层上的触控会导致底层元素滑动。

问题探究:

1、给body加overflow:hidden,pc端可以锁scroll,移动端无效
pc端可以直接overflow:hidden解决

2、给body加overflow:hidden及绝对定位,背景会定位到顶部,如果是单屏页面可以,长页面不适用
如果弹出浮层时背景本来就没有滚动距离,可以overflow:hidden加绝对定位解决

3、禁用touchmove事件,如@touchmove.prevent,对于弹层不需要的滑动的元素来说非常好用,因为scroll是touchmove触发的,直接禁用就不会滑动穿透了,其实是直接就没有系统滑动事件了。但是显然不适合弹层需要滑动的情况
如果弹层时不需要滚动的,可以直接禁用touchmove就可以了

4、专门解决滑动穿透的第三方,存在巨大的兼容性问题。比如tua-body-scroll-lock,android可以完美解决,ios整个屏幕都不能滑动了。高星的body-scroll-lock据说android全挂,就没有试了。
第三方有兼容性问题,可以自己判断ua选用

5、终极解决方案:vant的popup
合理完美的解决方案,不存在兼容问题,适用于任何情况的popup。如果你不想为了锁背景引入一个根本用不到的库,可以一起来研究下popup的实现原理。

原理探究

如果不想看源码想直接知道结论的话可以看这里:

因为常见会滑动穿透的场景都是:

  • 子元素本来就不可滚动,在子元素上滑动引起背景滚动,
  • 子元素可以滚动,但已经滚动到顶部或者底部,继续滑动的话就会滑动穿透

所以如果子元素本身不可滚动,或者子元素氪滚动,但已经滚动到顶部或者底部时直接对touchmove进行默认事件阻止就可以阻止滑动穿透了。因为scroll事件是通过touchmove触发的,禁止掉就不会触发系统的scroll事件了。这样就可以完美解决可滚动元素可以滚动但其背景在滑动时不为所动的效果了。

如果你想看看popup到底时如何做的可以来看看下面的源码:

源码分析:
src/popup/index.js文件中主要是参数及界面显示的处理。

// src/popup/index.js
import { createNamespace, isDef } from '../utils';
import { PopupMixin } from '../mixins/popup';
import Icon from '../icon';

const [createComponent, bem] = createNamespace('popup');

export default createComponent({
  // 穿透处理的代码在这里混入
  mixins: [PopupMixin],

  props: {
    round: Boolean,
    duration: Number,
    closeable: Boolean,
    transition: String,
    safeAreaInsetBottom: Boolean,
    closeIcon: {
      type: String,
      default: 'cross'
    },
    closeIconPosition: {
      type: String,
      default: 'top-right'
    },
    position: {
      type: String,
      default: 'center'
    },
    overlay: {
      type: Boolean,
      default: true
    },
    closeOnClickOverlay: {
      type: Boolean,
      default: true
    }
  },

  beforeCreate() {
    const createEmitter = eventName => event => this.$emit(eventName, event);

    this.onClick = createEmitter('click');
    this.onOpened = createEmitter('opened');
    this.onClosed = createEmitter('closed');
  },

  render() {
    if (!this.shouldRender) {
      return;
    }

    const { round, position, duration } = this;

    const transitionName =
      this.transition ||
      (position === 'center' ? 'van-fade' : `van-popup-slide-${position}`);

    const style = {};
    if (isDef(duration)) {
      style.transitionDuration = `${duration}s`;
    }

    return (
      <transition
        name={transitionName}
        onAfterEnter={this.onOpened}
        onAfterLeave={this.onClosed}
      >
        <div
          vShow={this.value}
          style={style}
          class={bem({
            round,
            [position]: position,
            'safe-area-inset-bottom': this.safeAreaInsetBottom
          })}
          onClick={this.onClick}
        >
          {this.slots()}
          {this.closeable && (
            <Icon
              role="button"
              tabindex="0"
              name={this.closeIcon}
              class={bem('close-icon', this.closeIconPosition)}
              onClick={this.close}
            />
          )}
        </div>
      </transition>
    );
  }
});

根据mixins混入,可以看到核心部分应该在src/mixins/popup中,在这里针对lockscroll做出了两种处理,绑定touchmove及touchstart并绑定class:van-overflow-hidden

// src/mixins/popup/index.js
import { context } from './context';
import { TouchMixin } from '../touch';
import { PortalMixin } from '../portal';
import { on, off, preventDefault } from '../../utils/dom/event';
import { openOverlay, closeOverlay, updateOverlay } from './overlay';
import { getScrollEventTarget } from '../../utils/dom/scroll';

export const PopupMixin = {
  mixins: [
    TouchMixin,
    PortalMixin({
      afterPortal() {
        if (this.overlay) {
          updateOverlay();
        }
      }
    })
  ],

  props: {
    // whether to show popup
    value: Boolean,
    // whether to show overlay
    overlay: Boolean,
    // overlay custom style
    overlayStyle: Object,
    // overlay custom class name
    overlayClass: String,
    // whether to close popup when click overlay
    closeOnClickOverlay: Boolean,
    // z-index
    zIndex: [Number, String],
    // prevent body scroll
    lockScroll: {
      type: Boolean,
      default: true
    },
    // whether to lazy render
    lazyRender: {
      type: Boolean,
      default: true
    }
  },

  data() {
    return {
      inited: this.value
    };
  },

  computed: {
    shouldRender() {
      return this.inited || !this.lazyRender;
    }
  },

  watch: {
    value(val) {
      const type = val ? 'open' : 'close';
      this.inited = this.inited || this.value;
      this[type]();
      this.$emit(type);
    },

    overlay: 'renderOverlay'
  },

  mounted() {
    if (this.value) {
      this.open();
    }
  },

  /* istanbul ignore next */
  activated() {
    if (this.value) {
      this.open();
    }
  },

  beforeDestroy() {
    this.close();

    if (this.getContainer && this.$parent && this.$parent.$el) {
      this.$parent.$el.appendChild(this.$el);
    }
  },

  /* istanbul ignore next */
  deactivated() {
    this.close();
  },

  methods: {
    open() {
      /* istanbul ignore next */
      if (this.$isServer || this.opened) {
        return;
      }

      // cover default zIndex
      if (this.zIndex !== undefined) {
        context.zIndex = this.zIndex;
      }

      this.opened = true;
      this.renderOverlay();
      // 穿透处理的核心部分
      if (this.lockScroll) {
        // 给touchstart及touchmove上绑定代码
        // 关于touchStart及ontouchmove的代码在TouchMixin的引入中
        on(document, 'touchstart', this.touchStart);
        on(document, 'touchmove', this.onTouchMove);

        if (!context.lockCount) {
          document.body.classList.add('van-overflow-hidden');
        }
        context.lockCount++;
      }
    },

    close() {
      if (!this.opened) {
        return;
      }

      if (this.lockScroll) {
        context.lockCount--;
        off(document, 'touchstart', this.touchStart);
        off(document, 'touchmove', this.onTouchMove);

        if (!context.lockCount) {
          document.body.classList.remove('van-overflow-hidden');
        }
      }

      this.opened = false;
      closeOverlay(this);
      this.$emit('input', false);
    },

    onTouchMove(event) {
      // 这个方法是touch文件中引入得,一会会看到
      // 主要计算滑动得方向及距离
      this.touchMove(event);
      // 方向计算
      const direction = this.deltaY > 0 ? '10' : '01';
      // 获取滚动目标对象
      const el = getScrollEventTarget(event.target, this.$el);
      // 滚动元素相关属性赋值
      const { scrollHeight, offsetHeight, scrollTop } = el;
      let status = '11';

      /* istanbul ignore next */
      if (scrollTop === 0) {
        // 没有滚动的情况下,判定是否有滚动条
        status = offsetHeight >= scrollHeight ? '00' : '01';
      } else if (scrollTop + offsetHeight >= scrollHeight) {
        // 有滚动距离且滚动到底部
        status = '10';
      }

      /* istanbul ignore next */
      if (
        status !== '11' &&
        this.direction === 'vertical' &&
        !(parseInt(status, 2) & parseInt(direction, 2))
      ) {
        // 有滚动条且有滚动距离且方向为垂直时,阻止默认事件,即阻止页面滚动
        // 所以原理其实是在可能会引起背景滑动穿透时禁止掉scroll事件
        // 因为常见会滑动穿透的场景都是子元素不滚动引起背景滚动,或者子元素已经滚动到顶部或者底部,继续滑动的话就会滑动穿透,如果发现已经滚动到顶部或者底部时直接禁止掉touchmove就可以阻止滑动穿透了
        preventDefault(event, true);
      }
    },

    renderOverlay() {
      if (this.$isServer || !this.value) {
        return;
      }

      this.$nextTick(() => {
        this.updateZIndex(this.overlay ? 1 : 0);

        if (this.overlay) {
          openOverlay(this, {
            zIndex: context.zIndex++,
            duration: this.duration,
            className: this.overlayClass,
            customStyle: this.overlayStyle
          });
        } else {
          closeOverlay(this);
        }
      });
    },

    updateZIndex(value = 0) {
      this.$el.style.zIndex = ++context.zIndex + value;
    }
  }
};

来看看touch的处理,可以看到给touchstart及touchmove绑定了滑动方向及距离得计算,touchmove这个方法会在ontouchmove中被调用,注意名称,不要混淆。

import Vue from 'vue';

const MIN_DISTANCE = 10;

function getDirection(x: number, y: number) {
  if (x > y && x > MIN_DISTANCE) {
    return 'horizontal';
  }

  if (y > x && y > MIN_DISTANCE) {
    return 'vertical';
  }

  return '';
}

type TouchMixinData = {
  startX: number;
  startY: number;
  deltaX: number;
  deltaY: number;
  offsetX: number;
  offsetY: number;
  direction: string;
};

export const TouchMixin = Vue.extend({
  data() {
    return { direction: '' } as TouchMixinData;
  },

  methods: {
    // touchstart获取起始位置
    touchStart(event: TouchEvent) {
      this.resetTouchStatus();
      this.startX = event.touches[0].clientX;
      this.startY = event.touches[0].clientY;
    },
    // touchmove算得移动后得位移差,用来计算方向和偏移量
    touchMove(event: TouchEvent) {
      const touch = event.touches[0];
      this.deltaX = touch.clientX - this.startX;
      this.deltaY = touch.clientY - this.startY;
      this.offsetX = Math.abs(this.deltaX);
      this.offsetY = Math.abs(this.deltaY);
      this.direction = this.direction || getDirection(this.offsetX, this.offsetY);
    },

    resetTouchStatus() {
      this.direction = '';
      this.deltaX = 0;
      this.deltaY = 0;
      this.offsetX = 0;
      this.offsetY = 0;
    }
  }
});

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

滑动穿透终极解决方案 的相关文章

  • Web 服务器内置 Azure 应用服务

    背景 我正在托管一个静态 Web 应用程序 由 NUXTJS 在 Azure 应用服务上生成 现在我想通过以下方式配置更多内容web config比如为多个域设置 CORS 设置 但我找不到任何文档来确定 Azure 应用程序服务中集成 安
  • 表单标签的 CSS 样式

    据我所知 一个
  • php 和 html 文件扩展名有什么区别?

    我有一个 php文件包含以下代码 当我将文件的扩展名更改为 html那么它也以同样的方式表现 任何人都可以解释以下内容 为什么文件的行为方式与两个文件的行为方式相同 扩展 两者有什么区别 php and html文件扩展名 php file
  • 允许获取请求但仅在我的域中?

    在我的网站上 我可以使用 GET 请求触发某些操作 例如隐藏或删除评论的功能 我不是很担心 但如果有人使用 img src url 设计攻击来删除评论或电子邮件 那会很烦人 有办法防止这种情况吗 我使用 httponlycookies 作为
  • 根据 $_POST 值填充字段

    我正在寻求有关我试图填写的表格的帮助 我通常会使用 onChange 函数 但我认为这不是一个选择 基本上我有一个
  • 没有样式表的 Z-Index

    您可以在不编写或嵌入样式表的情况下为 HTML 中的图像设置 Z Index 吗 我正在将一些内容上传到网络论坛 并且需要能够将一些文本放在图像顶部 并在表格内进行格式化 但图像只是被推到一边 没有办法 发送回 我一直在对 HTML 本身进
  • 用于网站的 Git / 接收后 / 测试站点和生产站点的分离

    我使用 Git 来管理网站的源代码和部署 目前测试站点和实时站点在同一个机器上运行 关注此资源http toroid org ams git website howto http toroid org ams git website how
  • IIS 6 网站根目录与应用程序?解决Url()?

    IIS 6 ASP NET 3 5 C NET 我们遇到一个问题 即同一组文件的行为会有所不同 具体取决于它是根 IIS 网站还是 IIS 网站下的应用程序 使用生成的网址解析网址 http msdn microsoft com en us
  • 如何将 html 输入到 Flask 中?

    我有这个 html 位
  • 使用 REST API 进行正确的会话管理

    我已经完成了 RESTful API 的设计 其中我使用作为参数发送的 API 令牌对每个请求进行身份验证 现在我想创建一个客户端界面 我想知道什么是管理每个客户端的会话的正确安全方法browser客户 我想过一个流程来保持服务器端无状态
  • Ruby On Rails - 在控制器中使用关注点

    可能的菜鸟警告 刚接触 RoR 我正在尝试在 RoR 中使用关注点 现在我只写了一个非常简单的问题 app controllers concerns foo rb module Foo extend ActiveSupport Concer
  • 我应该将标签存储在文本字段还是单独的表中?

    我有一个表 其中的行如下所示 id path tags 1 pictures pic1 jpg car bmw 3 pictures pic2 jpg cat animal pussy 4 pictures pic3 png gun 基本上
  • VSTS 构建失败/发布无法在 bin 文件夹中找到 roslyn\csc.exe

    我们有一个网站项目 安装了以下 nuget 软件包 Microsoft CodeDom Providers DotNetCompilerPlatform 1 0 8 Microsoft Net Compilers 2 4 0 The web
  • 使用“邮递员”chrome 应用程序的肥皂请求正文

    假日网络服务 的肥皂请求正文会是什么样子 http www holidaywebservice com HolidayService v2 HolidayService2 asmx wsdl http www holidaywebservi
  • Java selenium - 如何在 TimeoutException 之后正确刷新网页?

    ChromeOptions options new ChromeOptions options addExtensions new File extension 6 2 5 0 crx ZenMate options addExtensio
  • Magento:设置刚刚创建的网站的配置值?

    我正在以编程方式创建网站 用户等 问题是 创建网站时 我无法立即设置配置值 Code
  • Netty Nio java 中的通信

    我想在 Netty nio 中创建一个具有两个客户端和一个服务器的通信系统 更具体地说 首先 我希望当两个客户端与服务器连接时从服务器发送消息 然后能够在两个客户端之间交换数据 我正在使用本示例提供的代码 https github com
  • php 中的 stackoverflow 上有这样的成就系统吗? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 从概念上讲 如何使用 PHP 和 MySQL 为网站编写一个成就系统 唯一真正的方法是不断执行 MySQL 查询来测试成绩等吗 您有两
  • 烧瓶 - 404 未找到

    我是烧瓶开发的新手 这是我在烧瓶中的第一个程序 但它向我显示了这个错误 在服务器上找不到请求的 URL 如果您输入了网址 请手动检查拼写并重试 这是我的代码 from flask import Flask app Flask name ap
  • IIS 8 HTTPS/需要 SSL 导致超时错误

    尝试通过 IIS 8 通过 SSL 发布网站 但出现超时错误 任何帮助表示赞赏 采取的步骤 已验证该网站可以通过 HTTP 访问 http xxx xxx xxx xxx有效 此时使用 IP 地址 如果重要的话 IIS gt 服务器证书 g

随机推荐

  • 命令提示符的使用及运行Java程序

    常用的命令提示符 dir 列出当前目录下的文件以及文件夹 director md 创建目录 make director rd 删除目录 cd 进入指定目录 cd 退回到上一级目录 cd 退回到根目录 del 删除文件 del txt可以将所
  • c++11std::thread扩展

    最近 整理一下学习c 的文章 看到一篇文章 其中提到了thread local和std future 觉得这两东西很有趣 于是网上搜了一些资料 觉得很有帮助 希望可以对大家学习c 线程有所帮助 http www cnblogs com ha
  • 嵌入式设备文件系统构建——增加用户登录功能

    1 修改inittab文件 first run the system script file sysinit etc init d rcS 进入命令行 askfirst bin sh 添加执行登录验证 sysinit bin login c
  • 【毕设教程】随机森林算法

    文章目录 0 前言 1 什么是随机森林 2 随机森林构造流程 3 随机森林的优缺点 3 1 优点 3 2 缺点 3 3 随机森林算法实现 4 最后 0 前言 Hi 大家好 这里是丹成学长的毕设系列文章 对毕设有任何疑问都可以问学长哦 这两年
  • Firebug调试经验与技巧

    昨天网站出问题了1 为了调试cookie 特别找了关于firebug里面如何调试cookie的文章 觉得这篇不错 保留下来备份 Firebug调试经验与技巧 2009 03 13 15 22 16 转自 http blog sina com
  • redis,mysql,elasticsearch,hbase,hive对比区别,该如何选择

    几种数据库对比如下 redis mysql elasticsearch hbase hive 容量 容量扩展 低 中 大 海量 海量 查询时效性 极高 中等 较高 较高 低 查询灵活性 较差 非常好 较好 较差 非常好 写入速度 极快 中等
  • U3D通过按钮点击实现场景切换

    1 新建UI 选择button选项 新建button 2 file gt Build settings gt Add Open Scenes 把你当前场景添加进去 gt 把你想要切换的场景拖拽上去 3 新建一个空对象 挂载一个scenech
  • org.apache.http.ConnectionClosedException Premature end of Content-Length delimited message body

    最近生产环境报了这个系统异常 org apache http ConnectionClosedException Premature end of Content Length delimited message body expected
  • CANOE入门:DBC创建和编辑

    目录 dbc文件创建步骤 创建一个DBC数据库文件 创建网络节点Network nodes 创建Message 创建信号Signal 创建Signals用到的数值表Value Tables 将Value Tables关联到Signals 将
  • I/O error on GET request for "http://user-service/hi": user-service; nested exception is java.net.Un

    一 场景重现 最近闲暇时间打算系统学习下SpringCloud系统教程 毕竟最近微服务也挺火的 于是网上找了一个大牛的博客跟着一起学习 史上最简单的SpringCloud教程 一直跟着模仿构建SpringCloud一直也没出什么问题 直到在
  • Pgsql与Oracle语法差异(SQL迁移记录)

    oracle 数据库中没有limit关键字 LIMIT 1 替换为 rownum 1 select from table where rownum 1 输出1条 oracle 自增序列使用 sequence PGSQL 自增序列可用 ser
  • jquery笔记回顾

    jquery 1 jquery概念 js框架封装的原生的js代码 2 jquery版本区别及使用 jquery xxx js 有排版 体积大 jquery xxx min js 无排版 体积小 3 jquery与原生js对象进行互转 jqu
  • hk-bc.xyz forum.php,www.xavdz.com

    Domain Name XAVDZ COM Registry Domain ID 1838157110 DOMAIN COM VRSN Registrar WHOIS Server whois enom com Registrar URL
  • Kafka面试题

    Kafka核心总控制器Controller是什么 在Kafka集群中会有一个或者多个broker 其中有一个broker会被选举为控制器 Kafka Controller 它负责管理整个集群中所有分区和副本的状态 Controller选举机
  • 使用代理同步Chromium代码的心得

    先参看 http www chromium org developers how tos build instructions windows 非常坑爹 谷歌获取chromium源码的方式又变了 从chromium39 0 2313 2之后
  • poj 2155 Matrix

    Problem poj org problem id 2155 vjudge net contest 146952 problem A Meaning 一个 N N 的矩阵 A 初始时全部值为 0 有两种操作 1 C x1 y1 x2 y2
  • 电机驱动板发烫严重怎么办?一份大厂PCB布局指南参考

    作者 Pete Millett Technical Marketing Engineer Monolithic Power Systems 翻译 Toffee Jia 来源 MPS 电机驱动 IC 传递大量电流的同时也耗散了大量电能 通常
  • java远程连接linux并发送命令,两种方案比较Jsch与ganymed-ssh2

    通过Jsch连接 step 1引入jar包
  • curl学习2

    代理 什么是代理 Merrian Webster的解释是 一个通过验证的用户扮演另一个用户 今天 代理已经被广泛的使用 许多公司提供网络代理服务器 允许员工的网络客户端访问 下载文件 代理服务器处理这些用户的请求 libcurl支持SOCK
  • 滑动穿透终极解决方案

    问题描述 滑动穿透 浮层上的触控会导致底层元素滑动 问题探究 1 给body加overflow hidden pc端可以锁scroll 移动端无效 pc端可以直接overflow hidden解决 2 给body加overflow hidd