使用Vue手写一个对话框
作者:spriteApe
写在前面
相信大家之前都写过一些组件,尤其是这样的弹窗组件,没吃过猪肉还没见过猪跑嘛 哈哈哈~
有人可能会说,为什么要自己写,我就用ant-design或者饿了么的。sorry,我要的弹窗UI跟组件库的相差太远了,而且对话框组件通常都挂载在body下,要改样式的话得一个个去写全局的样式,会全局覆盖掉组件库的css…… 微调改样式就还好。如果组件库的对话框dom结构跟你想要的差了十万八千里,那也是佛祖难救……
基本实现
<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这个语法糖,用起来真香
组件中使用
下面给出一个基本示例
<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方式"食用"
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) }
使用方法
下面给出一个基本示例
showPosDialog({ title: '是否移除该商品?', onConfirm() { emits('delete') }, })
这样,我们就实现了一个组件既可以在template中使用,也可以在任何js中使用啦
初始不渲染
不知道大家有没有注意到,组件库的对话框在第一次打开之前,是没有挂载到body节点下的。上面我们封装的组件,如果有100个对话框,页面一开始就会在body下挂载100个节点,且都是实例化完成后的,增加了性能上的开销
投石问路
这可咋整,百度也不知道怎么问。一时间没有好的思路,我就去down一个ant-design-vue的源码看看。不得不说,这个项目是一层组件套一层,太鸡儿复杂了。功夫不负有心人,我在components/_util/Portal.tsx文件第69行看到了这么一行代码
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
具体实现
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的插槽
<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
showPosDialog({ // title: '是否移除该商品?', title: <div>vNode:是否移除该商品?</div>, onConfirm() { emits('delete') }, })
思考
实现过程没有那么顺畅,但也算是填补一部分技术空白吧。如果大佬有更好的想法,欢迎在评论区交流呀
别人都用hooks我为什么用工具函数
反正各有各的看法吧,用hooks可以方便用vue的响应式数据、生命周期钩子等等,但是只能在vue组件中去调用。我的实现没有用到vue的这些东西,所以就写成工具函数了,在哪都可以调用。
个人觉得,没有用到vue这些东西,没必要强制hooks化。
再看vue的hooks使用,实际上就是在构建之初实例化一次,后续的"消费"在于调用返回的函数。像我这种API的对话框,就是创建的时候实例化一次,关闭后就销毁了。没有复用一说,也就没必要写成hooks了
至于为什么不复用,如果同时存在两个API调起的对话框,你只有一个实例(复用方案),那无法满足需求了
总结
相比于我之前封装的组件,我觉得对话框属于略有特殊的组件吧。
- 得挂载body下防止样式层级受影响
- 需要支持API方式调用
- 初始不渲染
- 支持vNode传参
到此这篇关于使用Vue手写一个对话框的文章就介绍到这了,更多相关Vue对话框内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!