手撸一个对话框的心得

写在前面

相信大家之前都写过一些组件,尤其是这样的弹窗组件,没吃过猪肉还没见过猪跑嘛 哈哈哈~

有人可能会说,为什么要自己写,我就用ant-design或者饿了么的。sorry,我要的弹窗UI跟组件库的相差太远了,而且对话框组件通常都挂载在body下,要改样式的话得一个个去写全局的样式,会全局覆盖掉组件库的css...... 微调改样式就还好。如果组件库的对话框dom结构跟你想要的差了十万八千里,那也是佛祖难救......

基本实现

html 复制代码
<template>
  <Teleport to="body">
    <div
      class="popDialogMask"
      :style="{ zIndex: props.zIndex }"
      v-show="modalOpen"
      @click="props.maskClosable && (modalOpen = false)"
      v-bind="$attrs"
    >
      <div class="popDialogContent" @click.stop :style="{ width: props.width + 'px' }">
        <slot name="title">
          <div class="title">
            <div class="i-carbon:warning mr-2 color-#FF9A42" />
            {{ props.title }}
          </div>
        </slot>
        <slot name="content">
          <div class="content"></div>
        </slot>
        <slot name="footer">
          <div class="footer">
            <a-button
              type="primary"
              ghost
              @click="handleEdit('cancel')"
              v-if="cancelButtonVisible"
              >{{ props.cancelText }}</a-button
            >
            <a-button type="primary" @click="handleEdit('confirm')" :block="!cancelButtonVisible">{{
              props.okText
            }}</a-button>
          </div>
        </slot>
      </div>
    </div>
  </Teleport>
</template>

<script lang="ts" setup>
import { type IDiaLogProps } from './types'
const modalOpen = defineModel<boolean>('open')
const props = withDefaults(defineProps<IDiaLogProps>(), {
  title: '',
  maskClosable: false,
  cancelButtonVisible: true,
  zIndex: 2000,
  okText: '确定',
  cancelText: '取消',
  width: 640,
})
const emits = defineEmits(['confirm', 'cancel'])
const handleEdit = (type: Parameters<typeof emits>[0]) => {
  modalOpen.value = false
  emits(type)
}
</script>
<style lang="scss" scoped>
.popDialogMask {
  @apply fixed top-0 bottom-0 left-0 right-0  flex justify-center items-center;
  background-color: rgba(0, 0, 0, 0.45);
  .popDialogContent {
    @apply bg-white rounded-3xl  p-12 box-border;
    .title {
      @apply text-3xl flex justify-center items-center font-bold;
    }
    .content {
      @apply h-26;
    }
    .footer {
      @apply flex justify-between;
      :deep(.ant-btn) {
        @apply p-x-22.5 rounded-20 p-y-0 text-3xl;
        height: 80px;
        line-height: 80px;
      }
    }
  }
}
</style>

顺便我要讲一下vue的defineModel这个语法糖,用起来真香

组件中使用

下面给出一个基本示例

html 复制代码
    <pos-dialog
      v-model:open="posDialogVisible"
      title="我是title"
      :cancelButtonVisible="false"
    >
      <template #content>
        <div class="my-13 text-center">content</div>
      </template>
    </pos-dialog>

API方式调用

下面我们在utils中把组件引入,导出一个工具函数给我们后续API方式"食用"

ts 复制代码
export const showPosDialog = (option: typeof PosDialog.props) => {
  const modalWarp = document.createElement('div')
  const destroy = () => {
    // eslint-disable-next-line no-use-before-define
    modalInstance.unmount()
    //因为用了Teleport 我发现这里不写最好
    // document.body.removeChild(modalWarp)
  }
  const modalInstance = createApp(PosDialog, {
    ...option,
    open: true,
    //AOP
    onConfirm() {
      option.onConfirm && option.onConfirm()
      destroy()
    },
    onCancel() {
      option.onCancel && option.onCancel()
      destroy()
    },
  })
  modalInstance.mount(modalWarp)
   //因为用了Teleport 我发现这里不写最好
  // document.body.appendChild(modalWarp)
}

使用方法

下面给出一个基本示例

ts 复制代码
  showPosDialog({
    title: '是否移除该商品?',
    onConfirm() {
      emits('delete')
    },
  })

这样,我们就实现了一个组件既可以在template中使用,也可以在任何js中使用啦

初始不渲染

不知道大家有没有注意到,组件库的对话框在第一次打开之前,是没有挂载到body节点下的。上面我们封装的组件,如果有100个对话框,页面一开始就会在body下挂载100个节点,且都是实例化完成后的,增加了性能上的开销

投石问路

这可咋整,百度也不知道怎么问。一时间没有好的思路,我就去down一个ant-design-vue的源码看看。不得不说,这个项目是一层组件套一层,太鸡儿复杂了🤢。功夫不负有心人,我在components/_util/Portal.tsx文件第69行看到了这么一行代码

tsx 复制代码
    return () => {
      if (!shouldRender.value) return null;
      if (isSSR) {
        return slots.default?.();
      }
      //没错,就是这行!
      return container ? <Teleport to={container} v-slots={slots}></Teleport> : null;
    };

ant-design设计思路比较复杂,咱就不过多深究了,反正实现思路是有了。搞个组件包一层。如果是第一次且绑定值为false那就返回一个null

具体实现

tsx 复制代码
import { type IDiaLogProps } from './types'
import Dialog from './Dialog.vue'
export default defineComponent<IDiaLogProps & { open: boolean }>({
  name: 'PosDialog',
  inheritAttrs: false,
  props: Dialog.props,
  setup(props, { attrs, slots }) {
    const isFirstRender = ref(true)
    // 初始值为false的话需要监听第一次打开
    if (!props.open) {
      // 打开过一次dialog 后,将 isFirstRender 设置为 false
      const unWatch = watch(
        () => props.open,
        (val) => {
          if (val) {
            isFirstRender.value = false
            unWatch()
          }
        }
      )
    } else {
      isFirstRender.value = false
    }

    return () => {
      return isFirstRender.value ? null : <Dialog v-slots={slots} {...props} {...attrs} />
    }
  },
})

干这种活还是用tsx用起来比较顺手,写的时候注意把props、slot、attrs这些透传下去即可

vNode

其实在API方式调用时,我们还可以传vNode给组件。

修改组件

修改组件中需要支持vNode的插槽

html 复制代码
        <component :is="props.title" v-if="isVNode(props.title)"></component>
        <slot name="title" v-else>
          <div class="title">
            <div class="i-carbon:warning mr-2 color-#FF9A42" />
            {{ props.title }}
          </div>
        </slot>

使用vNode

tsx 复制代码
  showPosDialog({
   // title: '是否移除该商品?',
    title: <div>vNode:是否移除该商品?</div>,
    onConfirm() {
      emits('delete')
    },
  })

思考

实现过程没有那么顺畅,但也算是填补一部分技术空白吧。如果大佬有更好的想法,欢迎在评论区交流呀

别人都用hooks我为什么用工具函数

反正各有各的看法吧,用hooks可以方便用vue的响应式数据、生命周期钩子等等,但是只能在vue组件中去调用。我的实现没有用到vue的这些东西,所以就写成工具函数了,在哪都可以调用。

个人觉得,没有用到vue这些东西,没必要强制hooks化。

再看vue的hooks使用,实际上就是在构建之初实例化一次,后续的"消费"在于调用返回的函数。像我这种API的对话框,就是创建的时候实例化一次,关闭后就销毁了。没有复用一说,也就没必要写成hooks了

至于为什么不复用,如果同时存在两个API调起的对话框,你只有一个实例(复用方案),那无法满足需求了

总结

相比于我之前封装的组件,我觉得对话框属于略有特殊的组件吧。

  1. 得挂载body下防止样式层级受影响
  2. 需要支持API方式调用
  3. 初始不渲染
  4. 支持vNode传参
相关推荐
小马哥编程几秒前
Function.prototype和Object.prototype 的区别
javascript
小白学前端6661 分钟前
React Router 深入指南:从入门到进阶
前端·react.js·react
苹果醋31 分钟前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
web1309332039821 分钟前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
王小王和他的小伙伴24 分钟前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱28 分钟前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿33 分钟前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08211 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光931 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
隐形喷火龙1 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui