前言: 今天在项目中遇到了很多很多需要弹出一个对话框的场景,由于之前全都是通过 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.ts
文件,引入我们刚刚编写的Dialog
组件,一会儿我们就需要用到它了。 - 在此之前我们还需要引入两位老朋友
h
,函数和render
函数。在这里看过我之前《如何创建一个全局搜索框》 和《如何创建一个 Toast》 这两篇文章的朋友一定不会陌生这两个函数的意义,但为了照顾新朋友我还是会大概讲解一下这两个函数的主要用途的。 - 我相信大家对 Vue 渲染组件的流程有一个大概的认知,Vue 是先构建出 虚拟dom 然后再根据 虚拟dom 去渲染出 真实dom的。
- 在这里我们需要清晰的知道, Vue 给我们提供的的
template
标签仅仅只是一个让我们可以用熟悉的 html 标签书写 虚拟dom 的语法糖而已。
是的,你没有听错,它仅仅只是一个语法糖而已,它底层是会被编译成用h
函数创造出的 虚拟dom 在这里从而引出官方解释。 - 那么上文官方提到的渲染函数又是什么呢?其实就是刚刚我们提到的
h
函数。h()
函数更准确名字其实应该是createVnode()
,和它的英文翻译是一一对应的,创建虚拟Dom。 - 这个函数具体该如何使用呢?我们从实战去理解,让我们继续编写我们的
DialogCreator
类,我们创建两个函数,一个控制 dialog 的出现叫做present
方法,另一个控制 dialog 的消失,叫做dismiss
方法。 - 这里马上就要用到刚刚提到的
h
函数。h
函数的第一个参数可以接收一个组件作为实参,并且返回这个组件的 虚拟dom 给我们。所以我们可以按照下面的写法拿到我们所需要的 Dialog 组件的 虚拟dom。 - 拿到 虚拟dom 有什么用呢?这里需要引入我们的第二个关键函数
render
函数。我们需要知道,我们目前只拿到了一个游离于 真实dom节点 之外的一个“假的dom”节点,你需要告诉它该渲染到哪里。什么意思呢?打开我们的main.ts
文件。
千万不要忘记这个#app
是什么。
它就是我们全局唯一的 真实dom ,一个朴实无华的一个 id 叫做 app 的 真实dom。 - 然后我们观察我们
render
函数可以接收的参数类型是什么,看下图我画黄色线的地方,看到什么惊喜了吗?第一个参数是一个vnode
。
什么?vnode
,我刚刚不才通过h(Dialog)
函数拿到了一个vnode
吗?没错,聪明的你应该能猜到下面的写法了。 - emm 但是好像在报错,我们看一下错误信息。(这里我们忽略第三个参数,只考虑两个参数即可。)
,这个 container 参数的类型是一个element
或者ShadomRoot
,这又是什么鬼呢?我们继续点击render
函数,进入它的定义,发现 container 原来最终是一个HostElement
类型。看来这个搞清楚这个HostElement
是关键。
11.在这里我们转变一下思路,我们反向推断 HostElement
是个什么。让我们再次打开 main.ts
文件,这次我们跳进 mount
函数的定义,就是下面黄色圈圈圈起来的这个函数。
你看到了什么?
没错很熟悉的几个单词 HostElement
,注意,你千万不要觉得这个 HostElement
是什么很神奇的元素,让我们回想一下 mount
函数的参数是什么来着?
没错,还是那个普普通通的,一个叫做 app
的全局的真实dom。
- 由此我们可以反向推断出,
render
函数需要一个 真实dom 来包裹我们的虚拟dom。生产出一个 真实dom 还不容易吗?我们直接调取js
的方法,createElement(‘div’)
来生产一个普通的div
元素用来包裹我们的虚拟dom。
五. 完善 DialogCreator 类
- 现在也告诉了虚拟Dialog 组件该放在哪里了,接下来就需要将我们的
containerEl
放在正确的位置,放在哪里呢?由于我们的dialog
出现的情况一般都是最顶层。提醒你一下,别忘了我们所有其它页面都是被放到了id为 app
的div
标签里。那么为了保证它绝对出现在最顶层而不被其它页面遮挡的这种情况发生,那我们延伸一下思路,如果让我们的Dialog
成为body
标签的第一个子元素,并且由于之前我们给Dialog
组件设置了absolute
属性,那么它就会正好浮现在我们所有页面之上,由于它脱离了文档流,那么它的出现就不会影响我们其它页面的布局 - 思路有了,这还不简单吗?如何成为
body
的第一个子元素就是基础方法了,这里就不过多解释了。 - 而让元素消失的方法就更简单了,合适的时机移除这个 dom 元素即可。
- 让我们测试一下是否可行,我们随便在哪一个页面里去调用我们的
DialogCreator
类调用 new 生成一个Dialog
实例。然后随便写两个按钮去调用这两个方法测试一下。
效果如下:
但是由于我们的“遮罩”挡住了我们的按钮,所以导致现在我们点击不了消失按钮,下面环节是本文的重点,如果传递我们自定义的内容给dialog
,来灵活生成一个高度自定义的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 消失的方法
}