为什么要使用命令式组件
在我们的日常开发的场景中,不管是面对哪个端的系统,都会有一些删除的操作。而为了防止误操作,通常会在用户点击"删除"按钮后弹出一个删除确认对话框。
例如,一个管理系统的组织架构管理界面,当管理员点击"删除部门"按钮后,系统首先会弹出一个标题为"警告"的对话框,显示"您确定要删除该部门及其所有子部门吗?"等文字提示,并提供"确定"和"取消"两个选项供管理员二次确认。这一机制可以有效防止因误触或其他原因导致的重要数据被意外删除,从而确保系统的稳定性和安全性。
那么从上述业务场景出发,可以先来想一下,实现这个删除确认对话框组件需要哪些东西呢?
- 触发显示:在用户点击删除按钮时,需要将这个对话框显示出来。
- 对话框内容区域:通过上述对它需求的描述,该组件内部应该包含如下内容:
- 可修改的头部标题:可传入不同的头部标题(例如:"提示"或者"警告"等)。
- 可修改的提示信息:可传入不同的提示信息或者传入信息的主体(例如:"您确定要删除这条记录吗?")。
- 选择项:至少包含"确认"和"取消"两个按钮,以供用户去做出决策。
- 弹窗类型:根据提示的类型来写一些差异化的样式。
- 事件处理:
- "确认"按钮点击事件:当用户点击"确认"时,需要返回父级调用相应的删除逻辑,并在完成后关闭确认对话框。
- "取消"按钮点击事件:只需关闭确认对话框,不做任何删除操作。
声明式组件实现
根据上面的分析,先来用声明式组件实现一下该组件吧。
第一步: 在script里定义好所需参数并提供默认值,包括对话框标题(title
)、按钮类型(type
)、确认和取消按钮文本(confirmBtnText
和 cancelBtnText
)以及对话框是否显示(show
)。定义好组件所要触发的事件,这里包含 confirm
和 cancel
事件,点击对应按钮时会触发这些事件以通知父组件。
js
<script setup>
// 获取父组件传入参数
const props = defineProps({
title: { type: String, default: '提示' },
type: { type: String, default: 'prompt' },
confirmBtnText: { type: String, default: '确定' },
cancelBtnText: { type: String, default: '取消' },
show: { type: Boolean, default: false }
});
// 获取传入的方法
const emit = defineEmits(['confirm', 'cancel']);
// 定义取消方法
const cancel = () => {
emit('cancel');
}
// 定义确定方法
const confirm = () => {
emit('confirm');
}
</script>
第二步: 根据对话窗的需要,将dom结构分成上中下三部分,分别是标题、内容和操作按钮。绑定了 confirm
和 cancel
方法,并且根据父组件传入的type
,为按钮动态添加不同类型的样式。
html
<template>
<div class="deleteConfirmDialog" v-if="show">
<!-- 主体部分 -->
<div class="dialogMainBox">
<!-- 标题 -->
<div class="dialogHeader">{{ title }}</div>
<!-- 提示内容 -->
<div class="dialogContent">
<!-- 插槽 供用户插入自定义内容 -->
<slot></slot>
</div>
<!-- 操作按钮 -->
<div class="dialogFooter">
<!-- 确定按钮 -->
<button @click="confirm" :class="['btn', `btn-${type}`]">{{ confirmBtnText }}</button>
<!-- 取消按钮 -->
<button @click="cancel" class="btn">{{ cancelBtnText }}</button>
</div>
</div>
</div>
</template>
第三步: 给弹窗添加一些样式,这部分不再多说。
css
<style lang="scss" scoped>
.deleteConfirmDialog {
position: fixed;
top: 0px;
left: 0px;
width: 100vw;
height: 100vh;
background-color: rgb(138, 138, 138, .3);
.dialogMainBox {
background-color: #fff;
margin: 80px auto;
width: 400px;
border-radius: 4px;
overflow: hidden;
.dialogHeader {
background-color: #f2f2f2;
color: #666666;
font-size: 16px;
padding: 10px 15px;
}
.dialogContent {
padding: 30px;
}
.dialogFooter {
text-align: right;
padding: 20px;
.btn {
margin-right: 10px;
min-width: 70px;
border-radius: 3px;
font-size: 14px;
padding: 6px 10px;
border: 1px solid #000000;
background-color: #ffffff;
color: #000000;
&:hover {
color: #ffffff;
background-color: #666666;
border: 1px solid #666666;
}
}
.btn-warning {
color: #ffffff;
background-color: #ff7d00;
border: 1px solid #ff7d00;
&:hover {
color: #ffffff;
background-color: #dd6b00;
border: 1px solid #dd6b00;
}
}
}
}
}
</style>
声明式组件使用
接下来就可以在需要的地方使用了。引入后对DeleteConfirmDialog
组件进行配置,传递标题、类型、按钮文本、显示状态等相关属性,并在实例上绑定confirm
和 cancel
事件处理函数。还需要添加一个按钮来触发组件的显示。
js
<script setup>
import DeleteConfirmDialog from '../../components/DeleteConfirmDialog/index.vue';
import { ref } from 'vue';
// 控制弹窗显示/隐藏
const isDialogShow = ref(false);
// 删除按钮点击
const deleteItemClick = () => {
isDialogShow.value = true;
}
// 确认删除操作
const confirmHandel = () => {
isDialogShow.value = false;
alert('删除');
}
// 取消删除操作
const cancelHandel = () => {
isDialogShow.value = false;
alert('取消删除');
}
</script>
<template>
<div>
<button @click="deleteItemClick">删除</button>
<DeleteConfirmDialog
title="警示"
type="warning"
@confirm="confirmHandel"
@cancel="cancelHandel"
confirmBtnText="确定删除"
:show="isDialogShow"
>
<div>您确定要删除该部门及其所有子部门吗?</div>
</DeleteConfirmDialog>
</div>
</template>
点击删除按钮,即可以看到页面输出了该弹窗,见下图:
为什么要使用命令式组件
至此,我们已完成一个常规思路的弹窗组件封装。然而,这种方法在实际运用中存在一些局限性,特别是在系统中有大量类似需求的情况下。若每次需要使用该组件时,都需要单独进行引入组件、实例化、配置属性等操作,这一过程难免显得繁琐。因此,我们能否对其进行优化,使之只需通过调用一个方法就能便捷地实现对话框功能呢?
实际上,许多流行的 UI 组件库正是采用了这样的设计思路,比如 Element Plus 中的 MessageBox 消息对话框和 antd 中的 Modal 弹窗组件,它们都支持通过简单调用相应的方法即可快速展示弹窗界面,可以大大简化开发流程。那么,下面开始就着手对上面完成的组件进行一些改造吧。
怎么封装命令式组件
调用方法设计
基于上文中推断出的优化诉求,我们不妨设想一下到底使用什么样的调用方式来展现我们的弹窗组件才更加理想呢?目标是通过调用一个统一的方法,就像Element Plus
中通过 MessageBox
来唤起消息对话框那样,一次性传递所需参数,从而实现快速调用与灵活展示。所以这里借鉴 Element Plus
中 MessageBox
组件的成功实践,做出的设计如下:传递两个参数,第一个参数为弹窗所需的props,第二个参数是操作之后需要执行的回调函数。
之后将根据这里设计的调用需求 来具体的封装 DeleteConfirmDialog.alert
函数
js
<script setup>
import DeleteConfirmDialog from '../../components/DeleteConfirmDialog/index.js';
DeleteConfirmDialog.alert(
{
title: '警示',
type: 'warning',
confirmBtnText: '确定删除',
content: '您确定要删除该部门及其所有子部门吗?'
},
{
confirmCallback: (action) => {
console.log('关闭了');
},
cancelCallback: (action) => {
console.log('取消了');
},
}
)
</script>
封装开始的需要知道的前置知识:用到的Vue3 API了解
- createApp: 用来创建一个应用实例。它可以传递两个参数,第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props。
- app.mount(): 将应用实例挂载在一个容器元素中。应用实例必须在调用了
.mount()
方法后才会渲染出来。该方法接收一个"容器"参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串。应用根组件的内容将会被渲染在容器元素里面。 - app.unmount(): 卸载一个已挂载的应用实例。
原声明式组件改造
修改了调用方法之后,就需要对原组件进行对应的修改了。那么按照惯例还是需要先明确一下,原组件需要做哪些改造:
首先, 我们打算调整组件的加载与卸载机制,采用mount
和unmount
的方式来动态挂载和卸载组件,从而无需再通过v-if
指令来控制组件的显示与隐藏状态。
其次, 为了适应新的方法调用模式,不再利用emit
触发事件将取消和确定操作的信息传递给父组件,而是选择将这些事件处理函数直接作为props
传递给组件。
再者, 对于原来通过插槽传递的内容,现在我们将它改为接收一个明确的content
参数,直接在组件内部显示传入的内容。
最后, 在用户执行诸如"确定删除"这类操作后,我们需要执行两个核心动作:一是确保弹窗关闭,实现组件的及时卸载;二是执行用户在调用组件时传入的相关事件处理函数,继续执行后续逻辑(这里是执行删除操作)。
改造后代码如下(样式部分不变,省略):
js
<script setup>
// 参数传递
const props = defineProps({
title: { type: String, default: '提示' },
type: { type: String, default: 'prompt' },
content: { type: String, default: '内容' },
confirmBtnText: { type: String, default: '确定' },
cancelBtnText: { type: String, default: '取消' },
show: { type: Boolean, default: false },
cancel: { type: Function, default: () => {} },
confirm: { type: Function, default: () => {} },
});
</script>
<template>
<div class="deleteConfirmDialog">
<!-- 主体部分 -->
<div class="dialogMainBox">
<!-- 标题 -->
<div class="dialogHeader">{{ title }}</div>
<!-- 提示内容 -->
<div class="dialogContent">
{{ content }}
</div>
<!-- 操作按钮 -->
<div class="dialogFooter">
<!-- 确定按钮 -->
<button @click="confirm" :class="['btn', `btn-${type}`]">{{ confirmBtnText }}</button>
<!-- 取消按钮 -->
<button @click="cancel" class="btn">{{ cancelBtnText }}</button>
</div>
</div>
</div>
</template>
最后的封装
下面开始着手实现具体的 DeleteConfirmDialog.alert 方法。还是先确定一下具体的思路:
- 创建一个容器,并将其添加到
body
元素下面。 - 处理确认操作以及取消操作的函数,在函数内部执行卸载实例、移除容器以及执行传入的回调函数。并这两个函数作为属性对象添加到props对象中,等待创建实例的时候传入。
- 使用
createApp
API创建一个应用实例,将组件和属性对象作为参数传入。 - 将应用实例挂载到第一步创建的容器中,从而显示删除确认对话框。
具体实现如下(上述流程已添加具体注释)
js
import { createApp } from 'vue';
import DeleteConfirmDialog from './index.vue';
DeleteConfirmDialog.alert = (props, { confirmCallback, cancelCallback }) => {
// 创建一个容器 并将其添加到body下面
const containerBox = document.createElement('div');
document.body.appendChild(containerBox);
// 定义一个处理 确认删除操作 的函数
function confirm() {
// 卸载实例
DialogApp.unmount();
// 卸载容器
containerBox.remove();
// 执行传进来的方法
if (confirmCallback) confirmCallback();
}
// 定义一个处理 取消操作 的函数
function cancel() {
// 卸载实例
DialogApp.unmount();
// 卸载容器
containerBox.remove();
// 执行传进来的方法
if (cancelCallback) cancelCallback();
}
// 将函数放在props里传入组件中
props.confirm = confirm;
props.cancel = cancel;
// 调用createApp方法来创建一个应用实例
const DialogApp = createApp(DeleteConfirmDialog, props);
// 调用app.mount()方法 将DialogApp实例挂载创建的容器中来显示它
DialogApp.mount(containerBox);
}
export default DeleteConfirmDialog;
到这里,封装就完成了,经过上述的封装,现在在任何需要进行"二次确认删除 "操作的地方,只需简单地按照上文"调用方法设计"小节中设计的方法调用一个DeleteConfirmDialog.alert
函数,就可以快速而灵活地启动和管理二次确认对话框喽!
总结
本文通过扩展组件DeleteConfirmDialog
,新增了一个名为alert
的方法,实现了对该组件的命令式调用。当调用DeleteConfirmDialog.alert()
时,代码首先动态创建一个DOM容器并附加到网页body上,随后初始化一个应用实例,将DeleteConfirmDialog
组件挂载到这个容器中展示。
在方法内部,预先定义了确认和取消操作的处理函数,当用户在对话框中执行这两种操作时,会同步卸载组件实例、移除容器元素,并调用传入的对应回调函数(confirmCallback
和cancelCallback
)。这种方式简化了组件的调用和使用流程,使得开发者只需简单地调用一个函数,就能灵活地展示和控制二次确认对话框的行为,提高代码的可复用性和开发效率。