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

 更新时间:2022年07月27日 11:20:19   作者:weixin_41655541  
这篇文章主要介绍了vue中pc移动滚动穿透问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

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

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

相关文章

  • vue3.0实现考勤日历组件使用详解

    vue3.0实现考勤日历组件使用详解

    这篇文章主要为大家详细介绍了vue3.0实现考勤日历组件使用,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • vuejs+element-ui+laravel5.4上传文件的示例代码

    vuejs+element-ui+laravel5.4上传文件的示例代码

    本篇文章主要介绍了vuejs+element-ui+laravel5.4上传文件的示例代码,具有一定的参考价值,有兴趣的可以了解一下
    2017-08-08
  • 在vue中实现给每个页面顶部设置title

    在vue中实现给每个页面顶部设置title

    这篇文章主要介绍了在vue中实现给每个页面顶部设置title,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • Vue 自定义组件 v-model 使用详解

    Vue 自定义组件 v-model 使用详解

    这篇文章主要介绍了Vue 自定义组件 v-model 使用介绍,包括vue2中使用和vue3中使用,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • vue项目如何解决数字计算精度问题

    vue项目如何解决数字计算精度问题

    这篇文章主要介绍了vue项目如何解决数字计算精度问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • Axios在vue项目中的封装步骤

    Axios在vue项目中的封装步骤

    Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js,是一个第三方插件,第三方异步请求工具库,这篇文章主要介绍了Axios在vue项目中的封装方法,需要的朋友可以参考下
    2022-10-10
  • Axios学习笔记之使用方法教程

    Axios学习笔记之使用方法教程

    axios是用来做数据交互的插件,最近正在学习axios,所以想着整理成笔记方便大家和自己参考学习,下面这篇文章主要跟大家介绍了关于Axios使用方法的相关资料,需要的朋友们下面来一起看看吧。
    2017-07-07
  • vue-awesome-swiper 基于vue实现h5滑动翻页效果【推荐】

    vue-awesome-swiper 基于vue实现h5滑动翻页效果【推荐】

    说到h5的翻页,很定第一时间想到的是swiper。但是我当时想到的却是,vue里边怎么用swiper。这篇文章主要介绍了vue-awesome-swiper - 基于vue实现h5滑动翻页效果 ,需要的朋友可以参考下
    2018-11-11
  • 学习 Vue.js 遇到的那些坑

    学习 Vue.js 遇到的那些坑

    这篇文章主要介绍了学习 Vue.js 遇到的那些坑,帮助大家更好的理解和使用vue框架,感兴趣的朋友可以了解下
    2021-02-02
  • 如何使用vite搭建vue3项目详解

    如何使用vite搭建vue3项目详解

    Vite 是一个面向现代浏览器的更轻,更快的web应用开发工具,下面这篇文章主要给大家介绍了关于如何使用vite搭建vue3项目的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07

最新评论