Vue3 如何实现一个带遮罩的 dialog 对话框
前言: 今天在项目中遇到了很多很多需要弹出一个对话框的场景,由于之前全都是通过 v-if 来控制这个组件的显示与否,这样就造成了很多页面莫名多出了很多不相关的代码,极度不优雅。所以我尝试去实现了一个函数式调用的 dialog 组件,感觉在简单的场景下还是比较好用的,特来分享一下这个思路。🎁

一. 前期准备
你需要创建两个文件来和我一起完成这个函数式调用的 dialog ,
Dialog.vue 和 dialogCreator.ts。

二. dialog 遮罩的样式
-
我的组件样式是采用
UnoCss的写法,是将样式内嵌在标签的class属性里。和大家在Style标签里写是一模一样的效果,大家不用特别担心样式写法的问题,样式和本文主要内容没有任何直接的关系。 -
这里我们选择先写一个遮罩,关于遮罩的关键点其实就是需要设置一个带一点点透明度的背景,我选择了
rgba(0,0,0,0.4),也就是带0.4透明度的纯黑背景颜色。
在这里我们需要特别注意,由于我们的遮罩是会出现在“其它页面之上”的,所以我们需要给整个组件外部设置一个 absolute来使它独立于其它页面,为了防止某些边界情况,需要设置z-index:9999来保证这个页面会在整个应用之上。整体效果如下:
三. dialog 对话框的样式
-
关于 dialog 对话框的样式这里我们不统一设置,但是我们组件至少需要包含三个主要元素。一个 Header 区域,一个 content 区域,最后一个取消按钮和确定按钮的区域。

-
在这里你可以先把文字都暂时写成固定值,到后面我会解释如何通过 props 动态的传递这些值。
四. h 函数和 render 函数的用法
五. 完善 DialogCreator 类
-
现在也告诉了虚拟Dialog 组件该放在哪里了,接下来就需要将我们的
containerEl放在正确的位置,放在哪里呢?由于我们的dialog出现的情况一般都是最顶层。提醒你一下,别忘了我们所有其它页面都是被放到了id为 app的div标签里。那么为了保证它绝对出现在最顶层而不被其它页面遮挡的这种情况发生,那我们延伸一下思路,如果让我们的Dialog成为body标签的第一个子元素,并且由于之前我们给Dialog组件设置了absolute属性,那么它就会正好浮现在我们所有页面之上,由于它脱离了文档流,那么它的出现就不会影响我们其它页面的布局
-
思路有了,这还不简单吗?如何成为
body的第一个子元素就是基础方法了,这里就不过多解释了。
-
而让元素消失的方法就更简单了,合适的时机移除这个 dom 元素即可。

-
让我们测试一下是否可行,我们随便在哪一个页面里去调用我们的
DialogCreator类调用 new 生成一个Dialog实例。然后随便写两个按钮去调用这两个方法测试一下。
效果如下:
但是由于我们的“遮罩”挡住了我们的按钮,所以目前为止我们暂时点击不了消失按钮。别着急,我们一步一步尝试优化现在的代码。
六. 神奇的 h 函数
-
目前我们的
dialog已经可以出现到我们的页面了,但是现在它的内容都是写死的,不灵活,我们需要按照不同的场景传递不同的文字该如何实现呢?这里又需要请出我们的老朋友,h函数。 -
这里我先抛出概念,等等我们一步一步验证。
h函数是可以接收第二个参数的,并且第二个参数的值将被转换成 props 传递给我们的组件。
-
让我们回到
dialogCreator.ts文件。我们声明一个类型,准备作为DialogCreator内部constructor参数的类型。
并且声明两个类的属性 title和content来准备做为props传递给我们的Dialog.vue组件。
-
我们现在还缺少一个关键的东西,就是取消按钮和确定按钮的函数,我们一并声明。(这里需要注意,一般取消按钮就是关闭 dialog 对话框的功能,也就是类本身的 dismiss 方法,所以我们不需要用户额外提供取按钮的函数,只需要提供确定时的回调函数即可。)
这里需要读者仔细品味上图代码的含义。 -
接下来我们就需要传递
this.option给h函数即可。
报错了没关系,是因为我们还没有在 Dialog.vue组件内部定义Props。 -
让我们分别从
dialogCreator.ts文件导出这个DialogPropsType类型,再从Dialog.vue引入这个类型用来定义 props 即可。
随即可以看到我们刚刚到报错消失了,说明我们的思路是没问题的。

七. 改造 Dialog.vue 组件
-
我们先将之前固定写死的,
title部分和content部分替换成我们声明的 props 里的title和content。
-
然后别忘了我们
props还存放着《确定》和《取消》的的方法。取出来分别放置在这两个按钮身上。
-
随便找一个其它页面,测试刚刚的
DialogCreator类,内容我就随便自己写了
我们测试一下:

八. 遮罩的关闭效果
-
现在我们点击遮罩是没办法关闭 dialog 的,效果如下:

-
造成这种情况的原因也很简单,因为我们的遮罩没有点击事件,怎么办呢?非常非常简单,给遮罩添加取消
cancelBtn,也就是 dismiss 方法不就可以了吗?
测试一下,现在点击遮罩已经可以正常关闭 dialog 了。

九. 修复冒泡造成的 Bug
-
目前看起来功能已经很棒了,但是目前的代码会造成一个严重的
bug,我们在点击dialog本身的时候,由于事件冒泡,会错误的触发遮罩层的方法。
-
我们验证一下,我们随便编写一个函数,然后绑定到
dialog组件上。
注意:这里的 dialog指的是中间的那个实实在在的对话框本身,不是指整个组件。

-
然后给
cancelBtn也加一行console.log测试一下。
效果如下:

-
解决方法简单的出乎你的意料,让我们回到中间的
diaolog身上,仅仅只需要绑定一个空的click函数,然后加上修饰符stop即可。
-
效果如下,可以看到,现在点击
dialog已经不会错误的关闭整个 对话框了。
至此我们的 dialog组件已经可以在绝大部分场景下使用了。🎁~
总结
目前的代码只是一个很粗糙的实现,更加具体实用的功能还需读者根据自己项目的需求自行完成。下面是 DialogCreator.ts 文件的代码。读者可根据需要自行查阅。
import Dialog from "./Dialog.vue";
import { h, render } from "vue";
interface DialogType {
title: string;
content: string;
confirmBtn: () => void;
}
export interface DialogPropsType extends DialogType {
closeBtn: () => void;
}
export class DialogCreator {
containerEl: HTMLDivElement;
option: DialogPropsType;
constructor(option: DialogType) {
this.containerEl = document.createElement("div");
this.option = { ...option, closeBtn: this.disMiss.bind(this) };
}
present() {
const vnode = h(Dialog, this.option);
render(vnode, this.containerEl);
document.body.insertBefore(this.containerEl, document.body.firstChild);
}
disMiss() {
render(null, this.containerEl);
document.body.removeChild(this.containerEl);
} //dialog 消失的方法
}
转载自:https://juejin.cn/post/7182585412508975165