想象如下场景:
父组件里有三个弹窗,弹窗中是一个form+table的页面。
<template>
<div>
<a-modal>
<a-form></a-form>
<a-table></a-table>
</a-modal>
<a-modal>
<a-form></a-form>
<a-table></a-table>
</a-modal>
<a-modal>
<a-form></a-form>
<a-table></a-table>
</a-modal>
</div>
</template>
那父组件就需要三个变量控制弹窗开关。
const show1 = ref(false)
const show2 = ref(false)
const show3 = ref(false)
弹窗打开时需要调接口获取数据,弹窗关闭时需要调接口保存。
const data1 = {}
const data2 = {}
const data3 = {}
const onOk1 = () => {}
const onOk2 = () => {}
const onOk3 = () => {}
const onCancel1 = () => {}
const onCancel2 = () => {}
const onCancel3 = () => {}
这样写肯定不行。父组件里状态、方法、dom太多
把弹窗里的内容变成组件?
<a-modal>
<Comp1 />
</a-modal>
<a-modal>
<Comp2 />
</a-modal>
<a-modal>
<Comp3 />
</a-modal>
这样一来,如果a-modal在初始状态是关闭的,就不会加载其内部的组件的代码,父组件的加载就会变快。起到了异步组件的效果
但是,弹窗onOk、onCancel的时候,还要获取其内部的组件的数据,调接口。(或者直接调组件内部expose的方法)
这样父组件里就有三个开关状态,一堆弹窗动作与组件的交互,还是很乱!
如果把modal和内部dom一起封装成一个组件
<!-- 子组件HTML -->
<a-modal>
<a-form></a-form>
<a-table></a-table>
</a-modal>
// JS 部分
defineExpose({
// 暴露一个方法用来打开弹窗
showModal,
})
在父组件中:
<ModalComp1
ref="ModalCompRef"
/>
const openModal = () => {
ModalCompRef.value?.showModal()
}
这样很好,父组件变干净了,既不用维护控制弹窗开关的变量,也不用写弹窗开启和关闭时调接口的逻辑
只需要打开弹窗时ref调showModal方法,弹窗关闭时子组件emit事件中处理逻辑
但是父组件里就会有一个硕大的组件树,每个弹窗组件的代码都会和父组件一起加载!
我们可以把这整个组件变成一个异步组件,并加一个布尔值来控制组件初次打开
<div>
<ModalComp1
v-model="isShow"
v-if="isLoaded"
/>
</div>
const ModalComp1 = defineAsyncComponent(() => import('./ModalComp1.vue'))
// 用isLoaded控制父组件刚打开时不需要加载子组件
const isLoaded = ref(false)
const isShow = ref(false)
const openModal = () => {
// 初次打开弹窗时,isLoaded变为true,开始加载子组件的代码
if (!isLoaded.value) {
isLoaded.value = true
}
isShow.value = true
}
父组件加载时由于isLoaded是false,弹窗组件不会加载。
调openModal后,组件开始加载,加载完成后,由于isShow是true,弹窗会展示。
(为什么这里不用 组件ref.showModal 来打开?因为isLoaded变为true后,弹窗组件开始异步加载,不能确切的知道何时加载成功,此时无法用ref调用组件方法)
但是这样写,父组件里,每个弹窗都需要两个布尔值
其实isLoaded不是必须的,我们只需要用一个watch,在isShow初次变成true的时候,把isLoaded变成true就可以了。
加入一个hooks。
export default function useModalControl() {
const open = ref(false)
const loaded = ref(false)
const stopWatch = watch(open, (newVal, oldVal) => {
if (!oldVal && newVal) {
loaded.value = true
stopWatch()
}
})
return [open, readonly(loaded)] as const
}
在父组件中:
<div>
<ModalComp1
v-model="isShow"
v-if="isLoaded"
/>
</div>
const ModalComp1 = defineAsyncComponent(() => import('./ModalComp1.vue'))
import useModalControl from '@/composables/useModalControl'
const [isShow, isLoaded] = useModalControl()
const openModal = () => {
isShow.value = true
}
这下好了,既有了异步组件的能力,父组件也只需要维护一个isShow,但是这样又引入了一个hooks,组件上也多了一个v-if, 给后面看代码的人造成了疑惑
所以到底要怎样使用弹窗组件,才能做到:
1,弹窗内的数据和dom延迟加载
2,尽量减少父组件里的状态
3,弹窗内的数据可以直接emit到父组件
我自己倾向于,把状态和组件关联起来,决定 Modal 开关的状态,存在 Modal 组件内,通过作用域插槽来开关或者
defineExpose
开关都可以。然后是
showModal
,你是打算用 dialog 标签吗?dialog 标签初始就是加载出来的,我有一个方法就是,用v-if
来控制 dialog 的加载,使用Transition
组件的钩子来触发showModal
。比如说状态是isOpen
。isOpen
->true
-> dialog 渲染 -> 触发onEnter
钩子 -> 执行showModal()
isOpen
->false
-> dialog 移出 -> 触发afterLeave
钩子 -> 执行close()
同时还要在 cancel 事件里令
isOpen.value = false
就可以达到你说的效果了。补充:
大体是这样子的,如果用的是
showModal
可以用 ESC 关闭,然后在加上自己想要的样式就行了。