为什么要通过API创建Modal
在大多数基于Vue
的UI库中Modal组件的使用一般是类似下面的写法
<v-modal :visiable="showModal">
我们通过控制showModal
字段就可以来控制modal
窗口的显示.
但是这样写的问题一般有如下:
- 代码会入侵当前组件DOM
- 需要创建单独的状态变量来控制显示
- 关闭打开弹窗需要去手动重置组件内部数据
- 嵌套弹窗是一般需要进行一些特殊处理,如
$nextTick
以上这些问题都会使我们的代码变得复杂,我们希望理想的写法可能是如下:
onClick(){
modal.open(组件)
}
这样操作的行为可以和我们理解的更一致,弹窗代码只存在于对应的操作函数中,而不会如何dom,已经为它创建专门的控制变量.
但是具体应该如何做到通过函数调用而不用入侵DOM来显示弹窗呢?
通过创建Vue实例来创建Modal
在Vue如何需要显示组件一般需要在Template
区域或Render
函数添加改组件,而不能仅仅通过Api
来添加组件.
而在全局Api
中一般通过mount
来主动地进行vue实例的挂载,所有在Vue 2
中如果需要通过Api
方式来创建modal
一般的方法就是通过创建一个新的vue
实例来操作.
// 创建modal的容器
const container = document.createElement('div')
const el = document.createElement('div')
container.appendChild(el)
document.body.appendChild(container)
modal = new Vue({el,...})
这样modal就可以自动追加到body
的结尾位置,如果需要关闭modal
则可以通过destory
和removeChild
即可
modal.$destroy()
container.parentNode.removeChild(container)
可以既可以将添加的modal
从dom
上删除.
而组件可以通过Vue
的render
进行渲染显示即可.
但是这样做得方式也存在弊端,因为重新创建了vue
实例,所以和原有的vue
实例实际是两个单独的对象,所以store
、router
、mixed
、inject
需要重新注入新的实例中,否则在弹窗组件中无法正常使用部分功能,
具体实例代码如下:
import Vue from 'vue'
import { Modal } from 'ant-design-vue'
import { Observable } from 'rxjs'
export class ModalService {
/**
* 创建Modal容器
*/
private createModalContainer() {
const container = document.createElement('div')
const el = document.createElement('div')
container.appendChild(el)
document.body.appendChild(container)
return {
container,
el
}
}
/**
* 创建Modal组件
* @param options
*/
private renderModelComponent(Component, data, options) {
const { container, el } = this.createModalContainer()
let modalInstance
const modalClose = () => {
if (modalInstance && container.parentNode) {
modalInstance.$destroy()
container.parentNode.removeChild(container)
}
}
return new Observable<any>(subject => {
modalInstance = new Vue({
el,
render(h) {
return h(
Modal,
{
props: {
centered: true,
header: false,
...options,
visible: true,
footer: false
},
on: {
cancel: () => {
subject.complete()
modalClose()
}
}
},
[
h(Component, {
props: data,
on: {
'modal.submit': data => {
subject.next(data)
subject.complete()
modalClose()
},
'modal.cancel': () => {
subject.complete()
modalClose()
}
}
})
]
)
}
})
})
}
/**
* 弹出组件页面
* @param options
*/
public open(Component, data?, options?) {
return this.renderModelComponent(Component, data, options)
}
}
通过ModalService
的open
方法就可以打开弹窗组件了
const modal = new ModalService()
modal.open(Component)
通过使用Teleport来创建Modal
在Vue 3
中我们还有另一种选择,就是通过Teleport
可以将内容指定挂载到对应的位置
<teleport to="body">
...
不过相对于创建vue
实例来创建modal
的方式,这种方式我们需要提前安装容器来支持我们组件的显示,我们需要显式的将Teleport
写入模板才可以将其加载.
<template>
<modal-provider>
<router-view>
</modal-provider>
</template>
这样做得目的主要是为了一个是作为挂载modal
的容器,一个是可以向下传递provide
的内容,子组件可以通过inject('modal')
来获取对应的操作.
然后我们就可以将弹窗全部作为modal-provider
的子组件,然后通过动态组件来渲染需要弹窗的组件了
下来我们在modal-provider
组件可以通过teleport
来加载对应的modal-container
<template lang="pug">
slot
.modal-teleport
teleport(
to="body"
)
transition-group(name="modal-fade")
modal-container(
v-for="(modal,index) in modals"
:key="modal.id"
:id="modal.id"
:component="modal.component"
:params="modal.props"
:title="modal.config.title"
:closable="modal.config.closable"
:maskClosable="modal.config.maskClosable"
:min-width="minWidth"
:width="modal.config.width"
)
</template>
可以看到我们通过teleport
将所有弹窗挂载到了body
元素上,然后通过transition-group
来实现弹窗的显示动画效果,而modal-container
是我们用来显示内容的弹窗组件.
下来我们需要准备openModal
方法来讲来传入需要弹窗的组件和配置.
const modals = shallowRef<IModal[]>([]);
async function openModal(option: IModalOption) {
const component = defineAsyncComponent(() =>
Promise.resolve(option.component)
);
return new Promise((resolve) => {
modals.value.push({
id: Math.random().toString(32).slice(2),
component,
props: option.props,
resolve,
config: option,
});
triggerRef(modals);
});
}
modals
就是我们用来保存所有弹窗的数组,为了通过<component :is="...">
来显示动态显示传入的组件,我们通过defineAsyncComponent
来处理进行处理.
因为我们创建modal
实际返回了promise
对象,这样我们可以在关闭窗口通过执行Promise.resolve来做数据回传的功能.
另外在modal-container
中,主要的工作就是负责显示组件
<template>
.modal-container
.modal-wrapper(@click.self="maskClosable&&onCloseModal()")
.modal-content(:style="contentStyle")
.modal-header(v-if="header")
.title {{title}}
.action
img.close-button(
v-if="closable"
:src="closeSVG"
@click="onCloseModal"
)
.modal-body
component(:is="component" v-bind="params")
</template>
就这样基本完成了通过teleport
实现modal
的核心功能我进可以通过open
打开新的弹窗.
const modal = useModal()
modal.open(ModalOption)
对了还有一个问题是teleport
在SSR
中并不能很好的工作,会获得mismatch
的警告,如果要消除这个警告我们也需要做一些工作.
<template>
.modal-teleport(v-if="clientMounted")
teleport(
to="body"
)
</template>
<script lang="ts">
const clientMounted = ref(false)
onMounted(() => {
clientMounted.value = true;
});
</script>
这样做的目的是为了在mounted
我们并不加载teleport
组件,这样在SSR
中也就不会产生警告了,达到了client-only
效果.
具体实例代码可以查看如下地址: Github
如果你有好的方法,也希望能告诉我.