弹窗组件的封装
# 奇思妙想
在很早很早之前,弹窗的封装是基于数据副本渲染,那些弹窗 body 是写在内部的,如果说,弹窗 body 是用户定义的,应该怎么做呢,如果要做一个仅仅是弹窗的框架,内容由用户传进去渲染,这份内容是组件,而不再是基于数据渲染视图。于是基于这个想法,做了一个新的弹窗。
# createVNode
在参考 el-dialog 的写法后,业务需求使用视图的写法,有点局限性,由于调用方可能会在逻辑文件 js 中,故改用指令式调用,这时候使用 createVNode 去进行 render,是比较符合当前的场景,这样,我就可以在 js 文件中调用了。弹窗 ver1.0:
// createDynamicDialog.ts
import { VNode, createVNode, render, VNodeTypes, App } from "vue";
import DynamicDialog from "./DynamicDialog.vue";
/**
* 动态创建弹窗组件 通过createVNode
* @date 2022-05-25
* @returns {VNode}
*/
const createDynamicDialog = (app: App) => (
params: Record<string, any>,
child: VNodeTypes
): Promise<VNode | null> => {
const container = document.createElement("div");
// 实例销毁方法
const destroy = () => {
render(null, container);
document.body.removeChild(container);
};
/**
* 创建VNode 第三个参数是children
* 再利用createVNode创建children
* 组件内使用slot插槽渲染children的内容
* 这里默认使用default 若有更多插槽 可以继续添加其他项
*/
const vnode = createVNode(
DynamicDialog,
{
params,
destroy
},
// 创建动态子组件 通过createVNode
{
default: createVNode(child, {
params
})
}
);
// 存储上下文对象 须在render之前 不然组件内获取须在mounted里
vnode.appContext = app._context;
render(vnode, container);
document.body.appendChild(container);
};
export default createDynamicDialog;
2.下面是框架容器盒子,通过 slot 渲染插入的组件
<!-- DynamicDialog.vue -->
<template>
<div :class="$style.DynamicDialog">
<!-- 头部 -->
<header :class="$style.header">
<h3>{{ params.title }}</h3>
<i class="el-icon-circle-close" @click="destroy" />
</header>
<!-- 动态组件内容 -->
<slot />
</div>
</template>
3.使用
import PillarChart from "./components/PillarChart/PillarChart.vue";
// 这里就忽略 入口文件 全局注册弹窗了 这里是获取全局打开弹窗指令
const { $openDynamicDialog } = useGlobal();
// 通过第二个参数为child 这里是作为组件进行传参 body的内容就是用户定义的组件内容
$openDynamicDialog({ title: "经济态势数据分析" }, PillarChart);
这样我们就实现了用户传送组件到我们的弹窗 body 中了,这有点类似于 teleport 运输组件
# 弹窗 ver2.0
由于每次我们点击需要打开弹窗时,如果多次点击,那么他会多次插入到 body 中,也就是说会有多个弹窗出来,这明显不符合交互设计,所以我们需要将他改写成,每次点击只出最新的那一个。最简单粗暴就是打开之前,就将上一个给关闭掉:
let uid = 0;
const createDynamicDialog = (app: App) => (
params: Record<string, any>,
child: VNodeTypes
): VNode | null => {
// 生成dom id
const generateDomId = (id: number) => `dynamic-dialog-container${id}`;
// 上一个dom
const prevContainer = document.getElementById(generateDomId(uid));
if (prevContainer) {
render(null, prevContainer);
document.body.removeChild(prevContainer);
}
uid++;
const container = document.createElement("div");
container.id = generateDomId(uid);
// 实例销毁方法
const destroy = () => {
render(null, container);
document.body.removeChild(container);
};
const vnode = createVNode(
DynamicDialog,
{
params,
destroy
},
// 创建动态子组件 通过createVNode
{
default: createVNode(child, {
params
})
}
);
// 存储上下文对象 须在render之前 不然组件内获取须在mounted里
vnode.appContext = app._context;
render(vnode, container);
document.body.appendChild(container);
return vnode;
};
通过全局声明一个 uid,作为每个弹窗的 id 标识,每次打开之前,获取上一个 id 的弹窗,并且关掉,这样我们就实现了弹窗交互的单一开窗处理
# 弹窗 ver3.0
上面的 2.0 版本,确实帮我们解决了单一开窗,但是如果我们在点击相同的类型弹窗,他也会帮我们关闭上一个,再打开最新的那个,这样显然是多此一举,基于此,我们进行一个优化,弹窗 ver3.0:
import { VNode, createVNode, render, VNodeTypes, App } from "vue";
import DynamicDialog from "./DynamicDialog.vue";
/**
* 动态创建弹窗组件 通过createVNode
* @date 2022-05-25
* @returns {VNode}
*/
let uid = 0;
let prevPromise: any;
const createDynamicDialog = (app: App) => (
params: Record<string, any>,
child: VNodeTypes
): Promise<VNode | null> =>
/**
* 目的:解决点击相同类型弹窗不在开窗
* 实现:利用promise 做发布订阅 弹窗调用方式().then 就是destroy 执行的时机 方便穿插callback
*/
new Promise(resolve => {
// 生成dom id
const generateDomId = (id: number) => `dynamic-dialog-container${id}`;
// 上一个dom
const prevContainer = document.getElementById(generateDomId(uid));
if (prevContainer) {
render(null, prevContainer);
document.body.removeChild(prevContainer);
// 上一个的发布destroy 外层执行callback 注意这里要用prevPromise
prevPromise(null);
}
// 上一个调用者的订阅
prevPromise = resolve;
uid++;
const container = document.createElement("div");
container.id = generateDomId(uid);
// 实例销毁方法
const destroy = () => {
render(null, container);
document.body.removeChild(container);
// 发布destroy 外层执行callback
resolve(null);
};
const vnode = createVNode(
DynamicDialog,
{
params,
destroy
},
// 创建动态子组件 通过createVNode
{
default: createVNode(child, {
params
})
}
);
// 存储上下文对象 须在render之前 不然组件内获取须在mounted里
vnode.appContext = app._context;
render(vnode, container);
document.body.appendChild(container);
});
export default createDynamicDialog;
下面是使用:
// 调用者
let isOpen = false;
const handlerClick = () => {
if (isOpen) return;
$openDynamicDialog({ title: "经济态势数据分析" }, PillarChart).then(() => {
isOpen = false;
});
isOpen = true;
};
我们使用 promise 做发布订阅,这样调用者使用.then 的回调,就是关闭弹窗后的执行时机,将是否需要打开弹窗的逻辑交给调用者,调用者只需要设置一个 flag 就可以实现,同时在 close 时,初始回去 flag 的状态,否则就会出现问题,为什么将 flag 的设置交给调用方,之前的弹窗,我将这个设置是设置在了内部里,但是发现随着需求的增加,这里面的逻辑就会越发复杂,为了不污染内部,我将 close 的执行时机抛出去,将控制权交给调用者,你们想要在 close 做什么事情,你们自己决定,createDynamicDialog 只做他应该做的事就好。