最终效果:
// 函数式调用弹窗
showModal(ModalComponent, {props参数,emit事件})
为什么要封装弹窗系统
1.弹窗调用更简便
封装前使用:弹窗组件要定义打开弹窗的方法,再暴露给父组件。在父组件中,需要通过获取组件实例,再调用打开方法才能打开弹窗
<!-- 弹窗子组件 -->
<script setup lang="ts">
// 控制弹窗开关
const isShowModal = ref(false)
// 打开弹窗的方法
const open = () => {
isShowModal.value = true
}
// 向父组件暴露打开对话框方法
defineExpose({ openDialog })
</script>
<!-- 父组件 -->
<template>
XXX
<Modal ref="modalRef" />
</template>
<script setup lang="ts">
import Modal frmo './Modal.vue'
// 获取modal实例
const modalRef = ref<InstanceType<typeof Modal>>()
// 使用时需通过实例方法打开弹窗
modalRef.value?.open()
</script>
封装后使用:子组件仅需在关闭弹窗时触发close事件,父组件则是采用函数式调用
<!-- 弹窗子组件 -->
<script setup lang="ts">
const emit = defineEmits<{
// 关闭弹窗
close: []
}>()
//每当弹窗关闭时需要触发close事件,删除该弹窗节点
emit('close')
</script>
<!-- 父组件 -->
<script setup lang="ts">
// 引入showModal函数
import { showModal } from './utils/showModal'
// 使用showModal函数
showModal(component, {props参数,emit事件})
</script>
预备知识
h()
h函数的作用是用来创建虚拟DOM节点。
-
介绍
// 完整参数签名 function h( type: string | Component, // 可以是原生标签,也可以是组件 props?: object | null, // 要传递的参数 children?: Children | Slot | Slots // 子节点 ): VNode // 类型说明 type Children = string | number | boolean | VNode | null | Children[] // 插槽的作用主要是当h()第一个参数type为Component时,用来指定虚拟DOM渲染的位置 // 默认插槽--需要以插槽函数进行传递,即用箭头函数 type Slot = () => Children // 具名插槽--以插槽函数的对象形式来传递 type Slots = { [name: string]: Slot }
-
示例
创建原生标签
// 该DOM节点的类名,样式,内容等均可以在props中指定 // 下面例子中class: [foo, { bar }] 表示 class 属性是一个数组,数组中包含两个元素:foo、bar h('div', { class: [foo, { bar: true }], style: { color: 'red' }, innerHTML: 'hello'}) // 事件监听器应以 onXxx 的形式书写 h('div', { onClick: () => {} }) // children 数组可以同时包含 vnode 和字符串, 没有 prop 时可以省略不写 h('div', ['hello', h('span', 'hello')])
创建组件
import Foo from './Foo.vue' // 传递 prop, 此处是重点,可以传递props参数,也可以定义emit事件的回调 h(Foo, { // 等价于 some-prop="hello" someProp: 'hello', // 等价于 @update="() => {}" onUpdate: () => {} }) // 传递单个默认插槽 h(Foo, () => 'default slot') // 传递具名插槽--必须使用对象形式,为每一个具名插槽指定DOM // 注意,需要使用 `null` 来避免 // 插槽对象被当作是 prop h(MyComponent, null, { default: () => 'default slot', foo: () => h('div', 'foo'), bar: () => [h('span', 'one'), h('span', 'two')] })
-
具体使用--如何将VNode应用到页面上
声明渲染函数
import { ref, h } from 'vue' export default { props: { /* ... */ }, setup(props) { const count = ref(1) // 返回渲染函数 // 返回必须是一个函数而不是一个值!这能保证渲染函数实时更新 return () => h('div', props.msg + count.value) } }
由于渲染函数优先级,如果在setup()钩子中返回渲染函数,Vue将会忽略组件的
<template>
部分,转而使用setup()钩子返回的渲染函数
来生成组件的 DOM 结构
了解完h()之后,你需要知道:
-
h()的三个参数。
第二个参数如何传递props,定义emit回调。第三个参数中插槽的使用,用来指定子元素渲染的位置
-
返回的VNode节点如何使用
如何实现
公共utils
作用:
-
维护两个响应式变量
componentList
和nextComponentList
。componentList
用来储存正在渲染的弹窗,nextComponentList
用来存储待渲染的弹窗,在当前所有弹窗关闭后即会渲染 -
导出一个showModal(),用于打开弹窗
-
导出一个showNextModal(),用于呈现想要当前所有弹窗关闭后,再依次渲染的弹窗
-
导出一个clearComponentList(),用于清空两个弹窗列表
- 导出一个asyncModal(),用于将弹窗包装成异步弹窗
// modal.ts
import { ref, watch, shallowRef, defineAsyncComponent } from 'vue'
import type { Component, AsyncComponentLoader } from 'vue'
import LoadingModal from './components/LoadingModal/index.vue'
interface ITempModalItem {
component: Component // 这里应该是一个组件构造器或函数组件
props: object // 传递给组件的props,类型可以根据需要定义为更具体的类型
}
interface IModalItem extends ITempModalItem {
id: symbol // 组件编号,用来标识每个组件
}
// 存储当前渲染组件
export const componentList = ref<IModalItem[]>([])
// 存储待渲染组件
const nextComponentList = ref<ITempModalItem[]>([])
/**
* 展示弹窗
* @param component 弹窗组件
* @param props props参数或者emit事件回调
*/
export const showModal = (component: Component, props: any) => {
// 对传入的组件参数进行处理
const tempComponent = shallowRef(component)
// 为每个弹窗生成唯一标识
const id = Symbol()
// 添加一个关闭弹窗事件
props['onClose'] = () => {
// 通过id找到改组件在数组中的次序
const index = componentList.value.findIndex((item) => item.id === id)
if (index !== -1) {
// 删除组件列表对应元素
componentList.value.splice(index, 1)
}
}
componentList.value.push({ component: tempComponent, props, id })
}
/**
* 当前渲染的弹窗全部关闭时才进行渲染的弹窗
* @param component 弹窗组件
* @param props props参数或者emit事件回调
*/
export const showNextModal = (component: Component, props: any) => {
// 对传入的组件参数进行处理
const tempComponent = shallowRef(component)
// 将新增弹窗加入待渲染列表nextComponentList
nextComponentList.value.push({ component: tempComponent, props })
}
// 监视当前正在渲染的弹窗,若为空,则渲染待渲染弹窗
watch(
() => componentList.value.length,
() => {
if (nextComponentList.value.length && componentList.value.length === 0) {
// 待渲染弹窗不为空时,且当前正在渲染的弹窗列表为空,则将待渲染的弹窗列表添加到正在渲染的弹窗列表中
const nextModal = nextComponentList.value.shift()
if (nextModal) {
showModal(nextModal.component, nextModal.props)
}
}
}
)
/**
* 页面切换时,清空所有弹窗
*/
export const clearComponentList = () => {
componentList.value = []
nextComponentList.value = []
}
/**
* 将弹窗包装成异步弹窗
* @param loader 渲染函数,形如 () => import('./content.vue')
* @param loadingComponent 弹窗加载前loading状态展示的组件
* @returns 异步弹窗
*/
export const asyncModal = (loader: AsyncComponentLoader, loadingComponent?: Component) => {
const temComponent = defineAsyncComponent({
// 加载函数
loader,
// 加载异步组件时使用的组件
loadingComponent: loadingComponent || LoadingModal,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200
})
temComponent['preload'] = loader
return temComponent
}
组件ModalContainer
作用:从utils中获取弹窗列表,调用h函数,返回渲染函数
// ModalContainer.vue
<script>
import { componentList } from '@/utils/modal'
import { h } from 'vue'
import ModalWrapper from '@/components/ModalContainer/components/ModalWrapper.vue'
export default {
setup() {
return () =>
componentList.value.map((item) => {
return h(ModalWrapper, { key: item.id }, () => h(item.component, item.props))
})
}
}
</script>
组件ModalWrapper
作用:该组件为每一个弹窗组件的外层包裹组件,主要作用是抽离公共样式
// ModalWrapper.vue
<!-- 本组件为每个弹窗外包组件,仅提供布局--水平垂直居中 -->
<template>
<div id="modal-wrapper">
<slot></slot>
</div>
</template>
<style scoped>
#modal-wrapper {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10;
}
</style>
组件LoadingModal
作用:作为弹窗打开的默认加载组件
<!-- 本组件为弹窗加载默认组件,为菊花图动画 -->
<template>
<div class="juhua-loading">
<div class="jh-circle"></div>
<div class="jh-circle2 jh-circle"></div>
<div class="jh-circle3 jh-circle"></div>
<div class="jh-circle4 jh-circle"></div>
<div class="jh-circle5 jh-circle"></div>
<div class="jh-circle6 jh-circle"></div>
<div class="jh-circle7 jh-circle"></div>
<div class="jh-circle8 jh-circle"></div>
<div class="jh-circle9 jh-circle"></div>
<div class="jh-circle10 jh-circle"></div>
<div class="jh-circle11 jh-circle"></div>
<div class="jh-circle12 jh-circle"></div>
</div>
</template>
<style scoped>
.juhua-loading {
position: relative;
width: 40px;
height: 40px;
}
.juhua-loading .jh-circle {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.juhua-loading .jh-circle:before {
content: '';
display: block;
margin: 0 auto;
width: 15%;
height: 15%;
background-color: #333;
border-radius: 100%;
-webkit-animation: jh-circleFadeDelay 1.2s infinite ease-in-out both;
animation: jh-circleFadeDelay 1.2s infinite ease-in-out both;
}
.juhua-loading .jh-circle2 {
-webkit-transform: rotate(30deg);
-ms-transform: rotate(30deg);
transform: rotate(30deg);
}
.juhua-loading .jh-circle3 {
-webkit-transform: rotate(60deg);
-ms-transform: rotate(60deg);
transform: rotate(60deg);
}
.juhua-loading .jh-circle4 {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.juhua-loading .jh-circle5 {
-webkit-transform: rotate(120deg);
-ms-transform: rotate(120deg);
transform: rotate(120deg);
}
.juhua-loading .jh-circle6 {
-webkit-transform: rotate(150deg);
-ms-transform: rotate(150deg);
transform: rotate(150deg);
}
.juhua-loading .jh-circle7 {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.juhua-loading .jh-circle8 {
-webkit-transform: rotate(210deg);
-ms-transform: rotate(210deg);
transform: rotate(210deg);
}
.juhua-loading .jh-circle9 {
-webkit-transform: rotate(240deg);
-ms-transform: rotate(240deg);
transform: rotate(240deg);
}
.juhua-loading .jh-circle10 {
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.juhua-loading .jh-circle11 {
-webkit-transform: rotate(300deg);
-ms-transform: rotate(300deg);
transform: rotate(300deg);
}
.juhua-loading .jh-circle12 {
-webkit-transform: rotate(330deg);
-ms-transform: rotate(330deg);
transform: rotate(330deg);
}
.juhua-loading .jh-circle2:before {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.juhua-loading .jh-circle3:before {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.juhua-loading .jh-circle4:before {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.juhua-loading .jh-circle5:before {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.juhua-loading .jh-circle6:before {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.juhua-loading .jh-circle7:before {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.juhua-loading .jh-circle8:before {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.juhua-loading .jh-circle9:before {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.juhua-loading .jh-circle10:before {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.juhua-loading .jh-circle11:before {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.juhua-loading .jh-circle12:before {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes jh-circleFadeDelay {
0%, 39%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
@keyframes jh-circleFadeDelay {
0%, 39%, 100% {
opacity: 0;
}
40% {
opacity: 1;
}
}
</style>
添加路由守卫
作用:弹窗列表仅在单个页面有效,而弹窗列表是全局变量,故当切换其他页面时,应该清空弹窗列表
// router.vue
// 添加全局前置守卫
router.afterEach(() => {
clearComponentList()
})
添加弹窗组件出口
作用:使用渲染函数,设置弹窗出口
// 在App.vue中添加
<script setup lang="ts">
import ModalContainer from '@/components/ModalContainer/index.vue'
</script>
<template>
<router-view />
<ModalContainer />
</template>
如何使用
自定义弹窗子组件
<!-- 弹窗子组件 (./components/modal/content.vue) -->
<template>
<el-dialog v-model="dialogVisible" title="Demo" width="500" @closed="emit('close')">
<span>{{ props.modalContent }}</span>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirmEvent"> Confirm </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 控制弹窗显示
const dialogVisible = ref(true)
// 获取组件列表
const props = defineProps<{
// 文案
modalContent: string
}>()
// 自定义事件
const emit = defineEmits<{
// close事件用来销毁弹窗,当弹窗要关闭时调用emit('close')即可
close: []
// 确认弹窗
confirm: [text: string]
}>()
const confirmEvent = () => {
//关闭弹窗
dialogVisible.value = false
// 确认回调
emit('confirm', props.modalContent)
}
</script>
// 由于要封装成异步组件,所以需要再进行一步操作,再导出弹窗组件
<!-- 弹窗子组件 (./components/modal/index.ts) -->
import { asyncModal } from '@/utils/modal'
const Modal = asyncModal(() => import('./content.vue'))
// 如果弹窗过大,需要预加载,则调用 Modal.preload,进行预加载
// Modal.preload
export default Modal
使用弹窗的父组件
<!-- 父组件 -->
<template>
<button @click="openDialog">点击打开对话框</button>
</template>
<script setup lang="ts">
import Modal from './components/modal'
import AnotherModal from './components/AnotherModal'
import { showModal, showNextModal } from './utils/showModal'
const openDialog = () => {
// 打开弹窗
showModal(Modal, {
// props参数
modalContent: '我是二聪明',
// emit事件: 在组件内定义的emit事件名前加个on,即可指定emit回调
onConfirm: confirmDialogEvent
})
// 当前所有弹窗关闭后再的弹窗
showNextModal(AnotherModal, {})
}
// emit事件回调
const confirmDialogEvent = (text: string) => {
alert(`确认了内容为:${text} 的弹窗`)
}
</script>