手撸一个对话框的心得

写在前面

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

有人可能会说,为什么要自己写,我就用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传参
相关推荐
yuanyxh3 小时前
Mac 软件推荐
前端·javascript·程序员
万少4 小时前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木4 小时前
Web自动化测试
前端·python·pycharm·pytest
Kagol4 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel5 小时前
JavaScript 字符串与模板字面量:从表象到本质理解
前端
京东云开发者6 小时前
当AI成为导演-如何用AI创作动漫短剧
前端
李白的天不白6 小时前
使用 SmartAdmin 进行前后端开发
java·前端
乘风gg6 小时前
🤡PUA AI Coding 工具 的 10 条终极语录
前端·ai编程·claude
学Linux的语莫6 小时前
Vue 3 入门教程
前端·javascript·vue.js
怕浪猫7 小时前
第一章、Chrome DevTools Protocol (CDP) 详解
前端·javascript·chrome