🚀 这个 ElDialog 封装方案,让我的代码量减少了 80%

你是否也遇到过这样的场景:每次使用 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>

问题来了:

  1. ❌ 状态管理繁琐:每个对话框都需要一个 visible 状态
  2. ❌ 代码分散:打开对话框的逻辑和对话框组件分离
  3. ❌ 难以复用:每个对话框都要单独写模板
  4. ❌ 嵌套困难:在对话框内打开另一个对话框需要多层状态管理
  5. ❌ 无法获取返回值:无法像函数调用一样获取用户的选择结果

理想中的对话框应该是什么样的?

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 {}
};

技术实现解析

核心技术栈

  1. VueUse createTemplatePromise:将组件转换为 Promise
  2. Portal-vue:实现跨组件内容传输
  3. 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);
    }
  };
}

关键设计点

  1. 唯一标识符:每个 useBusDialog() 实例都有唯一的 sender,避免 Portal 冲突
  2. 自动清理:在 resolve/reject 时自动关闭对应的 Portal
  3. 优先级机制:渲染函数 > 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 共同分享和学习。

相关推荐
幽络源小助理2 小时前
SpringBoot+Vue雅苑小区管理系统源码 | Java物业项目免费下载 – 幽络源
java·vue.js·spring boot
谁是听故事的人2 小时前
vue前端面试指南
前端·vue.js·面试
小p2 小时前
nextjs学习1:回顾服务端渲染SSR
vue.js
Irene19913 小时前
Vue:defineProps、defineEmits、defineExpose 深度解析
vue.js·编译器宏
小徐不会敲代码~3 小时前
Vue3 学习 6
开发语言·前端·vue.js·学习
幽络源小助理3 小时前
SpringBoot+Vue多维分类知识管理系统源码 | Java知识库项目免费下载 – 幽络源
java·vue.js·spring boot
fengyucaihong_1233 小时前
vue加声音播放
javascript·vue.js·ecmascript
华仔啊3 小时前
Vue3 的设计目标是什么?相比 Vue2 做了哪些关键优化?
前端·vue.js
麦麦大数据3 小时前
F066 vue+flask中医草药靶点知识图谱智能问答系统|中医中药医学知识图谱
vue.js·flask·知识图谱·中医·草药·成分知识图谱·靶点