从零编写属于自己的组件库(Vue版)之遮罩层

3,301 阅读4分钟

开头

因为作者才开源了一个uni-app的组件库, 所以对编写组件还是有一点心得的。在此, 我会长期更新每个组件的编写, 各位可以在评论区评论想学习哪个组件, 后续都以文章的形式更新。当然只是自己的一点拙见, 可能不是最好的, 重在分享。让我们从一个简单的遮罩层开始吧。

组件设计

遮罩层是一个很常见的组件, 当然也能用在其他组件里面, 比如弹出框, toast轻提示等。所有在此我们以遮罩层为第一个组件, 开始我们的组件之旅。 组件编写我一般会从三个方面设计:

  1. 视图
  2. 数据
  3. 事件 那遮罩层的视图就很简单, 就是一个元素占满整个屏幕, 然后加一个背景颜色。如下图所示:

截屏2021-08-03 下午4.40.41.png 那我们的视图就很简单了, 就一个div或者view(uni-app里用view代替div), 下面我们皆以div为例.

 <div class='cc-mask'></div>

而它的样式也很简单, 我们给它固定定位, 上下左右4个方向皆为0即可。

<style scoped lang='scss'>
.cc-mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
</style>

好了, 现在页面上已经有一个占满整个屏幕的div了。但是它没有背景颜色, 所以我们还看不到它。但是背景颜色我们想要用户可以自定义, 所以我们先不着急加, 待会我们讲数据的时候再加。

有了这个div之后, 我们现在就要做淡入淡出的效果了, 在这我们就用纯css的动画方式实现。 首先我们默认的遮罩层是不显示的, 所以我们先把遮罩层的透明度即opacity设置为0, 在刚刚的css代码中加入

<style scoped lang='scss'>
.cc-mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  opacity: 0;
}
</style>

然后我们定义动画效果, 淡入的效果就是把透明度从0过渡到1, 淡出的效果就是把透明度从1过渡到0, 那用keyframes定义两个动画:

<style scoped lang='scss'>
@keyframes show {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes hide {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
</style>

动画定义完成我们在遮罩层的类名下加入这两个类名并且加上动画属性:

<style scoped lang='scss'>
.cc-mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  opacity: 0;
  &-show {
    animation: show 0.25s linear forwards;
  }
  &-hide {
    animation: hide 0.25s linear forwards;
  }
}
</style>

至此, 我们的视图和样式全部定义完成, 现在我们开始数据的设计。

首先我们要控制遮罩层的显示与隐藏, 我们需要一个变量, 但这个变量并不是在data里面定义的,而是需要从父组件传递过来, 那就需要在props里面去接收, 我们在props定义show变量, 类型是boolean, 默认值是false

export default {
   props: {
     show: {
       type: Boolean,
       default: show
     }
   }
}

注意, 这有一个坑, 我们要让遮罩层有淡入淡出的效果, 千万不能用v-if去控制, 因为v-if是直接销毁元素, 也不要用v-show去控制, 因为v-show是控制元素的display属性, 在实践过程当中我发现, 元素的display属性是和opacity属性不能共存的, 一旦把display设置为none之后就不能过渡的改变透明度了。那我们怎么做呢, 我们在data里面设置一个变量display为空, 然后在watch里面去监听父组件传递过来的show, 如果show为true, 则display为block, 如果show为false, 这有一个小技巧, 前面说了透明度不能和display共存, 我们在这用一个定时器, 延迟100毫秒之后将display设置为none即可, 代码如下:

watch: {
    show(val) {
      if (val) this.display = 'block'
        else {
          setTimeout(() => {
            this.display = 'none'
          }, 200)
        }
    }
  }

有了这个display属性之后, 我们就可以通过动态绑定样式的方式给我们的元素加上样式了, 并通过动态绑定class的方式将之前定义好的动画类名加上。

 <view
    class="cc-mask"
    :class="{ 'cc-mask-show': show, 'cc-mask-hide': !show }"
    :style="{ display }"
    @click="handleClick"
  >
    <slot></slot>

在此, 还有一个问题, 因为show默认是false, 所以我们一开始会个元素加上cc-mask-hide这个类名, 而这个类名是将透明度从1改变到0, 这样就会出现遮罩层先出现然后在变到没有的小bug, 这个时候我们在cc-mask-hide里面让元素的display: none即可, 代码如下:

<style scoped lang='scss'>
.cc-mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  opacity: 0;
  &-show {
    animation: show 0.25s linear forwards;
  }
  &-hide {
    animation: hide 0.25s linear forwards;
    &.cc-mask {
      display: none;
    }
  }
}
</style>

好了, 现在大体效果已经出现了, 我们继续完善我们的组件。我们的props里面陆续加上背景颜色, 元素层级, 遮罩层是否可点击等属性来让用户可以更多的自定义, 代码如下:

export default {
    props: {
       // 是否显示遮罩层
    show: {
      type: Boolean,
      default: false
    },
    // 层级
    zIndex: {
      type: [Number, String],
      default: 1
    },
    // 背景颜色
    bgColor: {
      type: String,
      default: 'rgba(0, 0, 0, 0.7)'
    },
    // 遮罩是否可点击
    maskClick: {
      type: Boolean,
      default: true
    },
    }
}

然后在元素上绑定, 最后我们给元素加上一个插槽, 让用户可以在遮罩层里面添加内容:

<view
    class="cc-mask"
    :class="{ 'cc-mask-show': show, 'cc-mask-hide': !show }"
    :style="{ zIndex, background: bgColor, display}"
    @click="handleClick"
  >
    <slot></slot>
  </view>

最后我们给元素加上点击事件,

 methods: {
    handleClick() {
      // 如果遮罩层不可点击 直接return
      if (!this.maskClick) return
      // 我们让父组件通过.sync传值 从而可以直接修改父组件的值
      this.$emit('update:show', !this.show)
      // 分发一个click事件给父组件 从而让父组件可以绑定事件
      this.$emit('click')
    }
  },

最后, 完整代码如下:

<template>
  <view
    class="cc-mask"
    :class="{ 'cc-mask-show': show, 'cc-mask-hide': !show }"
    :style="{ zIndex, background: bgColor, display }"
    @click="handleClick"
  >
    <slot></slot>
  </view>
</template>

<script>
export default {
  name: 'cc-mask',
  components: {},
  props: {
    // 是否显示遮罩层
    show: {
      type: Boolean,
      default: false
    },
    // 层级
    zIndex: {
      type: [Number, String],
      default: 1
    },
    // 背景颜色
    bgColor: {
      type: String,
      default: 'rgba(0, 0, 0, 0.7)'
    },
    // 遮罩是否可点击
    maskClick: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      display: ''
    }
  },
  methods: {
    handleClick() {
      // 如果遮罩层不可点击 直接return
      if (!this.maskClick) return
      // 我们让父组件通过.sync传值 从而可以直接修改父组件的值
      this.$emit('update:show', !this.show)
      // 分发一个click事件给父组件 从而让父组件可以绑定事件
      this.$emit('click')
    }
  },
  mounted() {
  },
  onLoad() {},
  onShow() {},
  filters: {},
  computed: {},
  watch: {
    show(val) {
      if (val) this.display = 'block'
        else {
          setTimeout(() => {
            this.display = 'none'
          }, 100)
        }
    }
  }
}
</script>


<style scoped lang="scss">
.cc-mask {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  opacity: 0;
  &-show {
    animation: show 0.25s linear forwards;
  }
  &-hide {
    animation: hide 0.25s linear forwards;
    &.cc-mask {
      display: none;
    }
  }
}
@keyframes show {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes hide {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
</style>

组件外使用该组件:

<div>
      <div>
        <cc-mask :show.sync="show1" @click="handleClick"></cc-mask>
        <cc-button @click="open1">打开遮罩层</cc-button>
      </div>

      <div>
        <cc-mask :show.sync="show2">
          <div class="content">
            <div class="block"></view>
          </div>
        </cc-mask>
        <button @click="open2">打开遮罩层</button>
    </div>
  </div>
  
<script>
export default {
  data() {
    return {
      show1: false,
      show2: false
    }
  },
  methods: {
    open1() {
      this.show1 = true
    },
    open2() {
      this.show2 = true
    },
    handleClick() {
      console.log('click')
    }
  },
}
</script>

<style scoped lang="scss">
.content {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  .block {
    width: 120px;
    height: 120px;
    background-color: #fff;
  }
}
</style>

打完收工。