你是否也遇到过这样的场景:每次使用 ElDialog 都要写一个 visible 变量,打开对话框要 visible = true,关闭要 visible = false,确认逻辑和对话框组件还得分开放?一个简单的确认对话框,动辄 30-50 行代码,嵌套对话框更是需要多层状态管理,代码变得臃肿难维护。
直到我遇到了 BusDialog,一个基于 Promise + Portal-vue 的 ElDialog 封装方案,彻底改变了我的开发方式。现在打开对话框就像调用函数一样简单:await openDialog({ content: '确认删除?' }),5 行代码搞定之前需要 50 行才能实现的功能。更重要的是,它支持跨组件内容组合、原生支持嵌套对话框,还自动处理状态清理,真正做到了零状态管理。
今天就来分享这个让我代码量减少 80% 的对话框封装方案,看看它是如何用 Promise 化 API 和 Portal-vue 重新定义 ElDialog 封装的。
为什么需要重新封装 ElDialog?
ElDialog 在管理系统中使用频率很高,但每次使用都要写大量样板代码:定义 visible、绑定 v-model、处理打开/关闭逻辑,一个简单的确认对话框动辄 30-50 行。更麻烦的是,每个对话框都需要一个独立的 visible 变量,嵌套对话框时状态管理更复杂。
如果能像 ElMessageBox 那样,一行代码就能打开对话框,用 async/await 处理确认和取消,那该多好?BusDialog 正是基于这个思路设计的封装方案。
传统封装的痛点
大多数开发者对 ElDialog 的使用和封装都是这样的:
vue
<template>
<el-dialog v-model="visible" title="提示">
<div>确认删除?</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确认</el-button>
</template>
</el-dialog>
</template>
<script setup>
const visible = ref(false);
const handleConfirm = () => {
// 处理确认逻辑
visible.value = false;
};
</script>
问题来了:
- ❌ 状态管理繁琐:每个对话框都需要一个 visible 状态
- ❌ 代码分散:打开对话框的逻辑和对话框组件分离
- ❌ 难以复用:每个对话框都要单独写模板
- ❌ 嵌套困难:在对话框内打开另一个对话框需要多层状态管理
- ❌ 无法获取返回值:无法像函数调用一样获取用户的选择结果
理想中的对话框应该是什么样的?
ts
// 理想:像函数调用一样简单
try {
await openDialog({ content: '确认删除?' });
// 用户点击了确认
console.log('删除成功');
} catch {
// 用户点击了取消或关闭
console.log('取消删除');
}
这就是 BusDialog 的设计理念!
BusDialog 的核心优势
1. 🎯 Promise 化 API:告别状态管理
传统方式:
vue
<script setup>
const visible = ref(false);
const handleDelete = () => {
visible.value = true;
};
const handleConfirm = async () => {
await deleteApi();
visible.value = false;
};
</script>
BusDialog 方式:
ts
const { openDialog } = useBusDialog();
const handleDelete = async () => {
try {
await openDialog({ content: '确认删除?' });
await deleteApi();
ElMessage.success('删除成功');
} catch {
ElMessage.info('取消删除');
}
};
优势:
- ✅ 零状态管理:不需要 visible、loading 等状态
- ✅ 代码更简洁:打开和确认逻辑在一起
- ✅ 错误处理清晰:使用 try/catch 处理取消操作
2. 🌐 Portal-vue 集成:跨组件内容组合
这是 BusDialog 最独特的功能!可以将来自不同组件的内容组合到同一个对话框中。
vue
<!-- 组件 A -->
<template>
<Portal :to="dialog.contentPortalName" :order="1">
<ProductSelector />
</Portal>
</template>
<!-- 组件 B -->
<template>
<Portal :to="dialog.contentPortalName" :order="2">
<PriceCalculator />
</Portal>
</template>
<!-- 使用 -->
<script setup>
const dialog = useBusDialog();
async function openComplexDialog() {
await dialog.openDialog({
title: '复杂表单',
// 内容由多个组件通过 Portal 注入
});
}
</script>
优势:
- ✅ 组件解耦:对话框内容可以来自多个独立组件
- ✅ 灵活组合:通过 order 控制渲染顺序
- ✅ 动态注入:可以在运行时动态添加内容
3. 🎨 三种内容设置方式,优先级清晰
BusDialog 提供了三种设置内容的方式,满足不同场景需求:
ts
// 方式 1:简单文本(最简单)
await openDialog({
content: '确认删除?'
});
// 方式 2:渲染函数(灵活)
await openDialog({
render: (resolve, reject) => (
<ComplexForm
onConfirm={resolve}
onCancel={reject}
/>
)
});
// 方式 3:Portal-vue(跨组件)
const dialog = useBusDialog();
<Portal :to="dialog.contentPortalName">
<MyComponent />
</Portal>
await dialog.openDialog();
优先级:渲染函数 > Portal-vue > 文本属性
4. 🔄 自动清理机制:零内存泄漏
传统封装需要手动管理对话框状态,容易造成内存泄漏。BusDialog 在关闭时自动清理所有 Portal 状态:
ts
// 自动清理,无需手动管理
const wrappedResolve = (value) => {
Wormhole.close({
to: 'bus-dialog',
from: sender,
});
originalResolve(value);
};
5. 🎭 完美兼容 Element Plus
通过 dialogConfig 可以访问 ElDialog 的所有原生功能:
ts
await openDialog({
title: '自定义对话框',
width: 800,
dialogConfig: {
// 所有 ElDialog 的原生属性都可以使用
draggable: true,
closeOnClickModal: false,
// ... 更多配置
}
});
6. 🎪 支持嵌套对话框
在对话框内打开另一个对话框?没问题!
ts
const handleNested = async () => {
try {
await openDialog({
content: '第一个对话框',
render: (resolve, reject) => (
<div>
<button onClick={async () => {
try {
await openDialog({ content: '嵌套对话框' });
resolve();
} catch {
reject();
}
}}>打开嵌套对话框</button>
</div>
)
});
} catch {}
};
技术实现解析
核心技术栈
- VueUse createTemplatePromise:将组件转换为 Promise
- Portal-vue:实现跨组件内容传输
- ulid:生成唯一标识符
核心实现思路
ts
export function useBusDialog() {
// 1. 生成唯一标识
const sender = ulid();
// 2. 创建 Promise 化的模板
const InnerDialog = createTemplatePromise<
BusDialogResult,
[BusDialogProps?]
>();
// 3. 包装 resolve/reject,自动清理 Portal
const createCloseHandler = (resolve, reject) => {
const wrappedResolve = (value) => {
Wormhole.close({ to: 'bus-dialog', from: sender });
resolve(value);
};
return { wrappedResolve, wrappedReject };
};
// 4. 返回 Promise 化的打开方法
return {
openDialog: async (args) => {
Wormhole.open({
to: 'bus-dialog',
from: sender,
content: BusDialog,
});
return await InnerDialog.start(args);
}
};
}
关键设计点
- 唯一标识符:每个 useBusDialog() 实例都有唯一的 sender,避免 Portal 冲突
- 自动清理:在 resolve/reject 时自动关闭对应的 Portal
- 优先级机制:渲染函数 > Portal-vue > 文本属性,确保灵活性
实际应用场景
场景 1:简单的确认对话框
ts
// 删除确认
const handleDelete = async (id: number) => {
try {
await openDialog({
content: '确认删除这条记录?',
title: '删除确认'
});
await deleteRecord(id);
ElMessage.success('删除成功');
} catch {
// 用户取消
}
};
场景 2:复杂表单对话框
tsx
// 编辑用户信息
const handleEdit = async (user: User) => {
try {
const result = await openDialog({
title: '编辑用户',
width: 600,
render: (resolve, reject) => (
<UserForm
initialData={user}
onSubmit={async (data) => {
await updateUser(data);
resolve({ reason: 'ok', data });
}}
onCancel={reject}
/>
)
});
ElMessage.success('更新成功');
} catch {}
};
场景 3:跨组件内容组合
vue
<!-- 产品选择器组件 -->
<template>
<Portal :to="dialog.contentPortalName" :order="1">
<ProductSelector v-model="selectedProducts" />
</Portal>
</template>
<!-- 价格计算器组件 -->
<template>
<Portal :to="dialog.contentPortalName" :order="2">
<PriceCalculator :products="selectedProducts" />
</Portal>
</template>
<!-- 使用 -->
<script setup>
const dialog = useBusDialog();
async function openOrderDialog() {
try {
await dialog.openDialog({
title: '创建订单',
width: 1000,
// 内容由多个组件通过 Portal 注入
});
} catch {}
}
</script>
总结
BusDialog 通过 Promise 化 API 和 Portal-vue 集成,重新定义了 ElDialog 的封装方式:
核心优势对比
| 特性 | 传统封装 | BusDialog |
|---|---|---|
| 状态管理 | ❌ 需要 visible 等状态 | ✅ 零状态管理 |
| 代码组织 | ❌ 打开和确认逻辑分离 | ✅ 逻辑集中,代码简洁 |
| 内容设置 | ❌ 只能在模板中写死 | ✅ 三种方式,灵活组合 |
| 跨组件 | ❌ 难以实现 | ✅ Portal-vue 轻松实现 |
| 嵌套支持 | ❌ 需要多层状态管理 | ✅ 原生支持 |
| 内存管理 | ⚠️ 需要手动清理 | ✅ 自动清理 |
适用场景
- ✅ 需要频繁使用对话框的场景
- ✅ 需要跨组件组合内容的复杂对话框
- ✅ 需要嵌套对话框的复杂交互
- ✅ 追求代码简洁和可维护性的项目
不适用场景
- ❌ 只需要简单展示的静态对话框(直接用 el-dialog 即可)
- ❌ 不需要获取用户选择结果的场景
如果你也在为对话框的状态管理而烦恼,不妨试试 BusDialog,让对话框像函数调用一样简单!
📦 GitHub: vben-business-components
📖 组件文档: BusDialog 详细文档地址
本文档内容由 AI 根据实际组件代码及官方文档自动生成,仅供学习和参考。如果你也有不错的业务组件封装案例,也可以联系 jenemy_xl 共同分享和学习。