Vue动态弹窗(Dialog)新境界:告别繁琐,拥抱优雅!

写在开头

嘿,各位好呀!

今是2025年04月30日,明天就是五一假期了,激动的心从早上就一直没有沉静过,午休的时候闭着眼半小时硬是没睡着,哎,这班是一点也上不下去。

好!说回正题,本次要分享的是关于如何 在 Vue 中比较优雅的调用弹窗 的过程,请诸君按需食用哈。

需求背景

最近,小编在捣鼓一个和低代码拖动交互类似的业务,说到低代码,大家肯定都不陌生吧❓像低代码表单、低代码图表平台等,用户可以通过简单 #技术分享的拖拽操作,像搭积木一样,快速"拼"出一个功能完善的表单页面,或者酷炫的数据可视化大屏。

而在这些低代码平台中,配置组件属性的 交互方式 通常有两种主流玩法:

其一,三栏式布局,左边是组件列表,中间是画布/预览区,右边是属性配置面板。选中中间画布的某个组件,右侧面板就自动显示它的配置项,如下:

其二,弹窗式配置,同样从左侧拖拽组件到画布,但选中组件后,通常会看到一个"设置"或"编辑"按钮。点击这个按钮,Duang~ ✨ 弹出一个专门的配置窗口 (Dialog),让你在里面集中完成所有设置。

这两种交互各有千秋,不评判好坏哈,反正合适自己业务场景的才是最好的。

然,今天咱们重点聚焦第二种:点击按钮弹出 Dialog 进行配置的场景。

这种方式在很多场景下也很常见,比如配置项特别多、需要更 沉浸式的设置体验 时。

但问题也随之而来:如果平台支持的组件越来越多,这里咱们假设是低代码图表场景,如柱状图、折线图、饼图、地图、文本、图片...等等,每个组件都需要一个独立的配置弹窗...,那么,我们应该如何设计一套优雅、可扩展、易维护的代码架构来管理这些层出不穷的 Dialog 呢?

结构设计

万事开头难,尤其是在做一些稍微带点设计或架构意味的事情时,切忌盲目上手。心里得先有个谱,想清楚大致方向,否则等到后面业务需求像潮水般涌来,迭代压力陡增时,你就会深刻体会到早期设计不佳带来的痛苦了(别问小编是怎么知道的...)。

当然,如果你已是经验丰富的老司机,那就当我没说哈。

面对"组件点击按钮弹出配置框"这个需求,最开始,最直观的想法可能就是:一个组件配一个专属的 Dialog.vue 文件,相互独立,互不影响,挺好不是❓

比如,咱当前有柱状图、折线图、饼图三个组件,那么它们的目录结构可能是这样子的:

src/
├── components/
│   ├── BarChart/
│   │   ├── Drag.vue  # 组件的拖动视图

│ │ ├── Dialog.vue # 组件的配置弹窗

│ │ └── index.js # 组件的 Model

│ ├── LineChart/ │ │ ├── Drag.vue # 组件的拖动视图

│ │ ├── Dialog.vue # 组件的配置弹窗

│ │ └── index.js # 组件的 Model

│ ├── PieChart/ │ │ ├── Drag.vue # 组件的拖动视图

│ │ ├── Dialog.vue # 组件的配置弹窗

│ │ └── index.js # 组件的 Model

└── App.vue # 入口

咱们不详说其他文件中的代码情况,仅关注每个组件中 Dialog.vue 文件的代码要如何写❓

可能大概是这样:




小编这里使用 Element-Plus 的 el-dialog 组件作为案例演示。

然后,为了在页面上渲染这些不同组件的 Dialog.vue ,最笨的方法可能是在父组件里面用 v-if/v-else-if 来判断,或者高级一点使用 再配合一堆 import 来动态加载渲染。父组件需要维护哪个弹窗应该显示的状态,以及负责传递数据和接收结果,逻辑很快变得复杂且难以维护。

在项目初期,组件类型少的时候,这种方式确实能跑通,没有问题❗

你就说它能不能跑吧,就算它不能跑,你能跑不就行,项目和你总有一个能跑的。

但随着业务不断迭代,支持的组件类型越来越多,这种"各自为战"的模式很快就暴露出了诸多问题,其中有两个问题比较尖锐:

总之,随着项目的迭代,这种最初看似简单的结构,维护成本越来越高,每次增加或修改一个组件的配置弹窗都成了一种"折磨"。

那么,要如何重新来设计这个架构呢❓

小编采用的是基于动态创建和静态方法关联的架构,其架构的核心理念就是:将通用的弹窗逻辑(创建、销毁、交互)抽离出来,让每个组件的配置面板(Panel)只专注于自身的配置项 UI 视图和数据处理逻辑 ,从而实现高内聚、低耦合、易扩展的目标。

先来瞅瞅目录结构的最终情况:

src/
├── components/
│   ├── BarChart/
│   │   ├── Dialog/
│   │   |   ├── index.js   # Dialog 组件的入口

│ │ | ├── Panel.vue # Dialog 组件 UI 视图

│ │ ├── Drag.vue │ │ └── index.js │ ├── LineChart/ │ │ ├── Dialog/ │ │ | ├── index.js # Dialog 组件的入口

│ │ | ├── Panel.vue # Dialog 组件 UI 视图

│ │ ├── Drag.vue │ │ └── index.js │ ├── PieChart/ │ │ ├── Dialog/ │ │ | ├── index.js # Dialog 组件的入口

│ │ | ├── Panel.vue # Dialog 组件 UI 视图

│ │ ├── Drag.vue │ │ └── index.js │ ├── BaseDialog.vue │ └── index.js ├── utils/ │ ├── BaseControl.js │ └── dialog.js └── App.vue # 入口

关键变动是 Dialog.vue 变成了 Dialog/index.jsDialog/Panel.vue ,它们俩的作用:

具体实现

接下来,咱们就详细拆解一下这套新架构的设计具体代码实现过程。

但为了更好的讲述关键代码的实现,咱们不管拖动那块逻辑,仅通过点击按钮简单的来模拟,效果如下:

本次小编是新建了一个 Vue3 的项目并且安装了 ElementPlus 进行了全局引入,基础项目环境就这样。

然后,从入口出发( App.vue ):




统一管理所有组件导出文件( components/index.js ):

import PieChart from "./PieChart";
import BarChart from "./BarChart";
import LineChart from "./LineChart";

export const componentMap = { [PieChart.type]: PieChart, [BarChart.type]: BarChart, [LineChart.type]: LineChart, };

组件入口文件(
components/PieChart/index.js
):

import BaseControl from "../../utils/BaseControl";
import Drag from "./Drag.vue";
import Dialog from "./Dialog";

class Component extends BaseControl { static type = "barChart"; label = "柱状图"; icon = "bar-chart";

getDialogDataDefault() { return { title: { text: "柱状图" }, tooltip: { trigger: "axis" }, }; }

static DragComponent = Drag; static DialogComponent = Dialog; }

export default Component;

该文件用于集中管理组件的核心数据结构与统一的业务逻辑。

咱们以柱状图为例哈。

所有组件的基类文件( utils/BaseControl.js ):

export default class BaseControl {

  type = "baseControl";

  label = "未知组件";

  height = "110px";
  constructor() {
    if (this.constructor.type) {
      this.type = this.constructor.type;
    }
  }

  static DragComponent = null;

  static DialogComponent = null;

dialog = {}; getDialogDataDefault() { return {}; } }

该文件是所有组件的"基石"️,每个具体的图表组件都继承自 BaseControl 类,并在该基础上定义自己特有的信息和逻辑。

组件的拖动视图组件( Drag.vue ),这个可以先随便整一个,暂时用不上:


Dialog 组件的入口文件(
components/BarChart/Dialog/index.js
):

import Panel from "./Panel.vue";
import { dialogWithComponent } from "../../../utils/dialog.js";

Panel.create = async (panelProps = {}) => { return dialogWithComponent((render) => render(Panel, panelProps), { title: panelProps.label, width: "400px", }); };

export default Panel;

该文件导入真正的 UI 视图面板( Panel.vue ),然后给组件挂载了一个静态 create 方法。这个 create 方法用于动态创建 Dialog 组件,它内部调用 dialogWithComponent 方法,并可以在此处预设一些该 Dialog 组件特有的配置(如默认标题、宽度)。

Dialog 组件的 Panel.vue 文件:




该组件仅放置柱状图特有的配置信息,并且不需要管弹窗自身的逻辑行为,很干净很专注。还有,它内部 必须 对外提供一个 getValue 方法❗用于在用户点击确认时调用,以获取最终的配置数据。

核心工具函数( utils/dialog.js )文件 :

import { createApp, h, ref } from "vue";
import { ElDialog, ElMessage } from "element-plus";
import BaseDialog from "../components/BaseDialog.vue";

export function dialogWithComponent(ContentComponent, dialogProps = {}) { return new Promise((resolve) => { const container = document.createElement("div"); document.body.appendChild(container); let vm = null; let loading = ref(false); const dialogRef = ref(null); const contentRef = ref(null);

const unmount = () => { if (vm) { vm.unmount(); vm = null; } document.body.removeChild(container); }; const confirm = async () => { let result = {}; const instance = contentRef.value; if (instance && instance.getValue) { loading.value = true; try { result = await instance.getValue(); } catch (error) { typeof error === "string" && ElMessage.error(error); loading.value = false; return; } loading.value = false; } unmount(); resolve(result); };

vm = createApp({ render() { return h( BaseDialog, { ref: dialogRef, modelValue: true, loading: loading.value, onDialogConfirm() { confirm(); }, onDialogCancel() { unmount(); }, ...dialogProps, }, { default: () => createVNode(h, ContentComponent, contentRef), }, ); }, });

vm.mount(container); }); }

export function createVNode(h, Component, ref = null) { if (!Component) return null; let instance = null; const render = (type, props = {}, children) => { return h( type, { ...props, ref: (el) => { if (ref) ref.value = el; }, }, children, ); }; if (typeof Component === "function") { instance = Component(render); } else { instance = render(Component); } return instance; }

dialogWithComponent 这个函数是整个架构的核心!它的职责就像一个专业的 Dialog "召唤师":

脑袋突然蹦出一句话:"去吧,就决定是你了,皮卡丘(柱状图)"

createVNode 这个函数是 Vue 中 h 函数的升级版本,它主要是帮忙做内容的渲染,它有两个小小的特点:

基础的 Dialog 组件文件( components/BaseDialog.vue ):




那么,整个核心代码的实现过程大概就是如此了。不知道你看完这部分的拆解,是否有了新的收获呢?

当然啦,在实际的业务场景中,代码的组织和细节处理会更加复杂,比如会涉及到更精细的状态管理、错误处理、权限控制、以及各种边界情况的兼容等等。这里为了突出咱们动态创建 Dialog 架构的核心思想,小编仅仅是把最关键的脉络拎了出来,并进行了一定程度的精简。

总结

总而言之,言而总之,这次架构的演进,给小编最大的感受就是️从"各自为战"到"统一调度"。

告别了维护繁琐、数量庞大的单个 Dialog.vue 文件,转而拥抱了基于 createApph 函数的动态创建方式。

这种新模式下,基础 Dialog、配置面板 ( Panel.vue )、以及调用逻辑各司其职,实现了真正的高内聚、低耦合。最终使得整个项目结构更加清晰、代码更加健壮,也极大地提升了后续的可维护性。希望这套方案能给你带来一些启发!

最后,如果你有任何疑问或者更好的想法,随时欢迎交流哦!


至此,本篇文章就写完啦,撒花撒花。

展开阅读全文

更新时间:2025-07-26

标签:科技   繁琐   优雅   动态   组件   代码   视图   文件   函数   架构   拖动   逻辑   入口   按钮

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020- All Rights Reserved. Powered By 71396.com 闽ICP备11008920号
闽公网安备35020302034903号

Top