vue中pc移动滚动穿透问题及解决

目录
  • vue pc移动滚动穿透问题
    • 上层无滚动(很简单直接@touchmove.prevent)
    • 上层有滚动
  • 滑动穿透终极解决方案
    • 问题描述
    • 问题探究
    • 原理探究

vue pc移动滚动穿透问题

上层无滚动(很简单直接@touchmove.prevent)

<div @touchmove.prevent>
我是里面的内容
</div>

上层有滚动

如果上层需要滚动的话,那么固定的时候先获取 body 的滑动距离,然后用 fixed 固定,用 top 模拟滚动距离;不固定的时候用获取 top 的值,然后让 body 滚动到之前的地方即可。

示例如下:

    watch:{
        statusShow(val){
            if(val) {
                this.lockBody();
            } else {
                this.resetBody();
            }
        },
        calendarShow(val){
            if(val) {
                this.lockBody();
            } else {
                this.resetBody();
            }
        }
    },

    methods: {
        lockBody() {
            const { body } = document;
            const scrollTop = document.body.scrollTop ||
            document.documentElement.scrollTop;
            body.style.position = 'fixed';
            body.style.width = '100%';
            body.style.top = `-${scrollTop}px`;
        },
        resetBody() {
            const { body } = document;
            const { top } = body.style;
            body.style.position = '';
            body.style.width = '';
            body.style.top = '';
            document.body.scrollTop = -parseInt(top, 10);
            document.documentElement.scrollTop = -parseInt(top, 10);
        },
}

body是DOM对象里的body子节点,即 标签;

documentElement 是整个节点树的根节点root,即 标签;

不同浏览器中,有的能识别document.body.scrollTop,有的能识别document.documentElement.scrollTop,有兼容性问题需要解决。

滑动穿透终极解决方案

问题描述

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

问题探究

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;
    }
  }
});

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • vue 弹框产生的滚动穿透问题的解决

    最近开发过程中遇到一些小问题(似乎问题总是那么多),但一直没什么时间去优化与解决.程序员不能被业务绑架,有时间还是花点在代码,开始这次的vue尝试吧. 首先定义一个全局样式: .noscroll{ position: fixed; left: 0; top: 0; width: 100%; } 创建一个dom.js文件,定义几个方法: export function hasClass(el, className) { let reg = new RegExp('(^|\\s)' + classN

  • vue中利用iscroll.js解决pc端滚动问题

    项目中经常遇到区域超出部分会出现滚动条,滚动条在pc端可以通过鼠标滚轮控制上下,在移动端可以通过鼠标拖动页面进行滚动,这两种场景都是符合用户习惯,然而这种滚动条一般都是竖[vertical]项滚动条,如果pc端出现横向滚动条[horizontal],在不做处理的情况下,你只能用鼠标拖动横向滚动条按钮[scrollerbar]展示滚动区域,而且为了美观,一般滚动条会进行样式编写或者隐藏,那么横向区域默认情况下就没法滚动. 二.描述 现为了解决pc端滚动区域能像移动端一样,能够通过鼠标拖动滚动区域直

  • Vue项目移动端滚动穿透问题的实现

    概述 今天在做 Vue 移动端项目的时候遇到了滚动穿透问题,在网上查资料后,选取了我觉得最好的方法,记录下来供以后开发时参考,相信对其他人也有用. 上层无需滚动 如果上层无需滚动的话,直接屏蔽上层的 touchmove 事件即可.示例如下: <div @touchmove.prevent> 我是里面的内容 </div> 上层需要滚动 如果上层需要滚动的话,那么固定的时候先获取 body 的滑动距离,然后用 fixed 固定,用 top 模拟滚动距离:不固定的时候用获取 top 的值

  • Vue解决移动端弹窗滚动穿透问题

    一.问题描述 在移动端的H5页面中,我们经常会遇到 "点击按钮-->弹窗-->选择选项" 这样的场景.而在选项过多出现滚动条时,滚动滚动条至容器的底部或者顶部.再往上或往下拖动滚动条时,滚动动作会出现穿透,这时候底部的body也会一起滚动. 问题总结:内容在滚动到容器的顶部或者底部时,再向上或向下 强行滚动 ,则出现滚动穿透 二.解决方案思考 参考了网上一大堆的解决方法,大可分为三类方法.经过认真的思考和分析,个人的总结如下: 使用js去控制和改变css 1. 弹窗出现 1

  • vue中pc移动滚动穿透问题及解决

    目录 vue pc移动滚动穿透问题 上层无滚动(很简单直接@touchmove.prevent) 上层有滚动 滑动穿透终极解决方案 问题描述 问题探究 原理探究 vue pc移动滚动穿透问题 上层无滚动(很简单直接@touchmove.prevent) <div @touchmove.prevent> 我是里面的内容 </div> 上层有滚动 如果上层需要滚动的话,那么固定的时候先获取 body 的滑动距离,然后用 fixed 固定,用 top 模拟滚动距离:不固定的时候用获取 t

  • 详解Vue中的scoped及穿透方法

    scoped的由来 css一直有个令人困扰的作用域问题:即使是模块化编程下,在对应的模块的js中import css进来,这个css仍然是全局的.为了避免css样式之间的污染,vue中引入了scoped这个概念. 在vue文件中的style标签上,有一个特殊的属性:scoped.当一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件.通过设置该属性,使得组件之间的样式不互相污染.如果一个项目中的所有style标签全部加上了scoped,相当于实现了样式的模块化. 但是这

  • Vue中消息横向滚动时setInterval清不掉的问题及解决方法

    最近在做项目时,需要进行两个组件联动,一个轮询获取到消息,然后将其传递给另外一个组件进行横向滚动展示,结果滚动的速度越来越快.这里记录一下来提醒自己.消息滚动的代码在最下面,方便下次使用. 问题背景: 最近在做一个需求,组件A获取消息采用的是轮询,组件A获取到新的消息后,将组件A中的消息传递给另外一个组件B,当组件B接收到消息时就让消息在页面上滚动播放. 实现思路: 这个项目应用的框架为VUE,当组件A获取到新的消息之后,就触发中央事件总线,在组件B中进行事件监听,将其添加进入一个数组,当判断定

  • vue中PC端地址跳转移动端的操作方法

    需求:pc端和移动端是两个独立的项目,两个项目项目中的内容基本相同,链接组合的方式都有规律可循,接到的需求便是在移动端访问pc端的URL连接时,重定向至移动端对应页面. 这个需求实现的方式比较明了,我的大致思路是在路由守卫处监听每个进来的路由请求,分析该请求是否是由移动端访问,若不是,则该路由请求直接放行:若是则分析要进入的路由路径,提取路径中的必要字段,组合称新的移动端链接即可. 里面涉及到了三个知识点:1.路由守卫,2.客户端判断.3.正则提取文字,接下来就分别按照这几点讲解一下,并附上整个

  • vue中element组件样式修改无效的解决方法

    如下所示: <style> .detail{ .el-input__inner { height: 48px; } } </style> 直接写style注意不加scoped,然后用一个组件最外层的class包裹住,就不会改到所有的组件的样式了. 以上这篇vue中element组件样式修改无效的解决方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们. 您可能感兴趣的文章: Vue 组件间的样式冲突污染 浅谈vue中改elementUI默认样式引发的st

  • Vue中android4.4不兼容问题的解决方法

    1.npm安装 npm install babel-polyfill npm install es6-promise package.json中会出现 "babel-polyfill": "^6.26.0", "es6-promise": "^4.1.1", 2.main.js引入 import 'babel-polyfill' import Vue from 'vue' import Es6Promise from 'es6

  • 在vue中使用el-tab-pane v-show/v-if无效的解决

    我就废话不多说了,大家还是直接看代码吧~ 解决方法如下: <template> <el-tabs v-model="settype" @tab-click="tabClick" ref="tabs"> <el-tab-pane label="广告位设置" name="bannerset">广告位设置</el-tab-pane> <el-tab-pane

  • 关于vue中使用three.js报错的解决方法

    目录 前言 1.vue的问题? 2.Proxy的异常情况 3.Three.js的问题 4.defineProperty异常情况 5.解决 总结 前言 最近在学习three.js,同时也学习一下vue3,然后就出现问题了,报错直接用不了,错误信息如下: Uncaught TypeError: 'get' on proxy: property 'modelViewMatrix' is a read-only and non-configurable data property on the prox

随机推荐