阅读 118

移动端滚动穿透分析及解决

背景

在WEBVIEW的H5页面中如果有弹窗,滚动操作一般会穿透弹窗蒙层,而可以滚动弹窗下面的窗口;

分析

  1. 在PC端我们一般通过使用 position: fixed 来控制弹窗的绝对定位
  2. 弹窗组件的接口一般如下:
.dialog-mask {
  position: fixed;
  top: 0;
  width: 100%;
  height: 100vh;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.5);
}


<section class="dialog-mask">
   <div class="dialog-content"></div>
</section>
复制代码

一般会有一个半透明蒙层,而内容在蒙层中绝对定位 或者 通过flex定位
3. 这个在PC端展示就没有问题,但是在手机端就会出现弹窗后背景可以滚动的问题,就是所谓的穿透
4. 那么如果我们让背景也和弹窗一样处于position:fixed的状态是否就可以了呢,答案是肯定的,但是我们有几情况如果不注意,就会带来副作用

副作用

  1. 一般我们很容易想到把body当做背景,我们在弹窗展示的时候,设置body的position为fixed,这时候会发现如果body内容被撑开,背景还是可以滚动,那么我们再把overflow设置为hidden;会发现并没有什么效果
  2. 那么我们会想到再把html当做背景,设置html的position为fixed,再把overflow设置为hidden;会发现在webview中背景固定了,但是背景页面会滚动到顶部,如果不做处理,关闭弹窗后背景页面位置变化了;另外一个副作用就是在有的浏览器这个处理也没有什么效果

最终解决

  1. 一般来讲,我们会把dialog和文档内容放在同一个容器,其实我们只需要设置dialog的父容器的样式即可解决这个滚动穿透的问题:
<section class="scroll-test" style="position: fixed; overflow: hidden;">
    <div class="page"></div> 
    <section class="dialog-mask">
           <div class="dialog-content">
              <div class="inner-content"></div>
           </div>
    </section>
</section>
复制代码
  1. 我们在弹窗时设置父元素的position和overflow,同时父容器需要设置width:100%;height:100%;这样可以保证设置fixed的时候内容不会滚动到顶部,从而解决最终问题,最终代码(VUE版本)如下:
<!--
 * @Author: wahrheit
 * @Date: 2020-08-17 19:19:43
 * @LastEditTime: 2021-05-14 11:41:04
 * @LastEditors: wahrheit
 * @Description: 弹窗组件
 * @FilePath: \xxx\src\components\mask.vue
 * 一往无前的唯一动力就是热爱你所做的一切
-->
<style lang='less' scoped>
.dialog-mask {
  position: fixed;
  overflow: hidden;
  top: 0;
  width: 100%;
  height: 100vh;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.5);
}
</style>
<template>
  <section class="dialog-mask" v-show="!close">
    <slot></slot>
  </section>
</template>
<script>
export default {
  name: 'DialogMask',
  props: {
    close: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      offsetHeight: 0
    }
  },
  watch: {
    close(newVal, oldVal) {
      const container = this.$el.parentElement
      if (newVal === false) {
        container.style.position = 'fixed'
        container.style.overflow = 'hidden'
      } else {
        container.style.position = 'absolute'
        container.style.overflow = ''
      }
    }
  },
  destroyed() {
    const container = this.$el.parentElement
    container.style.position = 'absolute'
    container.style.overflow = ''
  }
}
</script>



<!--
 * @Author: wahrheit
 * @Date: 2021-05-13 14:52:08
 * @LastEditTime: 2021-05-13 15:58:22
 * @LastEditors: wahrheit
 * @Description: 父组件测试
 * @FilePath: \xxx\src\containers\test\scrollTest.vue
 * 一往无前的唯一动力就是热爱你所做的一切
-->
<style lang='less' scoped>
section {
  position: relative;
  margin: 0;
  width: 100%;
  height: 100%;
  min-height: 100vh;
  font-size: 0;
  overflow: auto;
  box-sizing: border-box;
  -webkit-overflow-scrolling: touch;
  * {
    box-sizing: border-box;
  }
  .page {
    width: 100%;
    height: 200vh;
    background: linear-gradient(45deg,red,green);
  }

  .dialog-content {
    width: 80%;
    height: 60vh;
    border-radius: .2rem;
    background: #fff;
    position: absolute;
    left: 0;
    right: 0;
    margin: 0 auto;
    top: 50%;
    transform: translateY(-50%);
    overflow: scroll;
    .close-btn {
      position: absolute;
      width: .8rem;
      height: .8rem;
      border-radius: 50%;
      background: blue;
      color: #fff;
      font-size: .23rem;
      display: flex;
      align-items: center;
      justify-content: center;
      right: .2rem;
      top: .2rem;

    }
    .inner-content {
      width: 100%;
      height: 100vh;
      background: yellow;
    }
  }
}
</style>
<template>
  <section class='scroll-test'>
    <div class="page" @click="show=true"></div>
    <dialog-mask :close="!show">
      <div class="dialog-content">
        <a href="javascript:;" class="close-btn" @click="show=false">
          关闭
        </a>
        <div class="inner-content"></div>
      </div>
    </dialog-mask>
  </section>
</template>
<script>
import Mask from 'components/mask.vue'
export default {
  name: 'ScrollTest',
  data() {
    return {
      show: false
    }
  },
  components: {
    dialogMask: Mask
  }
}
</script>
复制代码

以上只处理了单层嵌套弹窗情况,如果弹窗组件被很深层次的子组件引用,需要指定相应的父组件,大家可以尝试做个兼容

  

文章分类
前端
文章标签