Vue3从0到1组件开发-基础组件:Mask

2,039 阅读5分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

Mask:不要不在乎遮罩

Why is mask?

在一个完整的组件库中,抽出Mask作为一个单独的组件仍然是可有可无的一种处理方法。而将Mask暴露给开发者使用的似乎更少,多见于主攻移动端的UI库当中,如Vantuniapp(下载插件) 等,而在主攻Web端的UI库当中则及其少见。当然也是因为Web端很少功能需要用到这类的组件。

此处所指的"完整的组件库"指能满足一个项目的基本功能需求的组件库。

但是在我所设想的组件库当中还是把它作为一个单独的组件库来开发,其中之一的原因在于我想把我的项目兼容移动端的平台,所以在为后面铺路。

并且Mask组件也是基础组件中比较有深度的一个组件了,除了正常的布局以外,还需要考虑比Button组件更复杂一点点的应用场景,例如点击事件是否触发点击事件禁止页面滚动,等一些小细节。

结构主体

前面提了个梗概,那么接下来,我们要做的就是用代码丰满它。

还是先把主要结构写出来。

<template lang="pug">
block content
div(
  v-if="isShow"
  class="yx-mask"
  @touchmove.stop.prevent="clear"
)
  div(
    class="yx-mask-class"
    :style="{backgroundColor: `${bgColor}`}"
    @click="cancel"
  )
    div(
      class="yx-mask__wrapper-box"
      @click="clear"
    )
      slot
</template>

尽管在基础组件中算是有点复杂点的组件了,但毕竟还是基础组件,所以结构还是停简单的,所以需要完成的事件也比较简单。

但是在完善事件之前,这里还是要贴一下它的css代码。毕竟有一部分的逻辑处理是涉及到了css部分的。

.yx-mask-class{
  position: fixed;
  left: 0; right: 0;
  top: 0; bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 99;
}
.yx-mask__wrapper-box{
  position: relative;
  z-index: 100;
}

.yx-mask-class作为父级,其css部分主要处理的内容涉及到占满当前屏幕层级控制(z-index),以及通过display:flex及其配套的css属性justify-content: center;,lign-items: center;实现子级也就是内容部分的绝对居中了。

实际开发时,因为考虑到移动端的关系,还加上了通过属性控制顶部弹出、底部弹出,侧边栏效果,但主要是类名+css控制, 这部分可以自由发挥。

上面这些就完成了一个基本的遮罩,在没有JavaScript完善逻辑的情况下,也俨然是一个正常的遮罩了。

接下来就是需要完善它的逻辑部分了。

逻辑部分

这个组件的逻辑部分写起来其实也不复杂,主要点在于思考上面。

如果需要完成通过属性控制顶部弹出、底部弹出,侧边栏效果则需要多一个函数控制最中返回的类名,但是这里简化处理,就只分享一下如何完善主要的逻辑部分了。

<script setup>
import { defineEmit, defineProps, ref } from 'vue';

const props = defineProps({
  show: {
    type: Boolean,
    default: true
  },
  animation: {
    type: Boolean,
    default: true
  },
  // 控制弹出方式, 在这个简化版中实际没有用到
  type: {
    type: String,
    default: 'center'
  },
  // 控制是否点击遮罩触发取消事件
  maskCancel: { 
    type: Boolean,
    default: true
  },
  bgColor: {
    type: String,
    default: 'rgba(0,0,0, .3)'
  }
})

const emits = defineEmit(['cancel','update:show']);

const isShow = ref(props.show);

document.body.style.overflow = 'hidden';

const clear = e => e.stopPropagation();

const cancel = () => {
  if(props.maskCancel){
    isShow.value = false;
    emits('update:show', false);
    emits('cancel');
    document.body.style.overflow = 'visible';
  }
}
</script>

在理解他做了什么之前,要注意几个代码中的细节, 我默认是通过props中的show来控制了遮罩的显示隐藏,但是为了方便web使用,我默认是给了一个true, 而在移动端这个组件才真正的需要通过传值来控制。

以及在这里的显示隐藏中,emits接受了一个事件cancel,一个v-model属性show,前者触发取消事件,后者用于父级传值控制时,反馈给父组件隐藏的值false。

显示时,禁止滚动

如果不处理这一点的话,默认的遮罩层打开后,在web端仍可以通过鼠标滚动来完成界面的滚动,这就很不美了。

所以这里添加了document.body.style.overflow = 'hidden';来控制当显示遮罩的时候,界面不允许滚动。

点击内容区不触发取消事件

可以看到,整个结构+逻辑中,埋伏了一个点击事件,但是我们只希望当有内容时,且仅点击透明遮罩部分才触发取消事件。

但是点击内容部分应该是不触发任何遮罩组件的事件的

这里就需要通过e.stopPropagation();来阻止冒泡了,所以事件clear() 只做了一件事,阻止冒泡。

完整的取消事件

在代码的最下面,可以看到完整的取消事件,当触发事件时,通过props.maskCancel属性判断是否允许点击遮罩

如果允许,触发时当前组件隐藏,并且默认是父组件传值控制,所以需要触发emits('update:show', false);并传值false给父级;

然后通过emits('cancel');触发父级的取消事件;

最后的最后控制bodyoverflow属性重新恢复默认状态(该滚滚,不能滚就不滚);

const cancel = () => {
  if(props.maskCancel){
    isShow.value = false;
    emits('update:show', false);
    emits('cancel');
    document.body.style.overflow = 'visible';
  }
}

最后

如果只是考虑Web端,确实没有单独写一个Mask组件的的必要,整个库及项目中只在少数几个地方用到, 需要时单独写也不麻烦。

但即便是不需要这样一个组件,也是需要考虑一下如果使用的话,应该怎么去处理才能更好用。

另外在本文中的组件与我实际做的略微修改,仅供参考,自由发挥。