写在开头
嘿,各位好呀!😀
好!说回正题,本次要分享的是关于如何在Vue中比较优雅的调用弹窗的过程,请诸君按需食用哈。
需求背景
最近,小编在捣鼓一个和低代码拖动交互类似的业务,说到低代码,大家肯定都不陌生吧❓像低代码表单、低代码图表平台等,用户可以通过简单的拖拽操作,像搭积木一样,快速"拼"出一个功能完善的表单页面,或者酷炫的数据可视化大屏。
而在这些低代码平台中,配置组件属性的交互方式通常有两种主流玩法:
其一,三栏式布局,左边是组件列表,中间是画布/预览区,右边是属性配置面板。选中中间画布的某个组件,右侧面板就自动显示它的配置项,如下:
其二,弹窗式配置,同样从左侧拖拽组件到画布,但选中组件后,通常会看到一个"设置"或"编辑"按钮。点击这个按钮,Duang~ ✨ 弹出一个专门的配置窗口 (Dialog),让你在里面集中完成所有设置。
这两种交互各有千秋,不评判好坏哈,反正合适自己业务场景的才是最好的。
然,今天咱们重点聚焦第二种:点击按钮弹出 Dialog
进行配置的场景。
这种方式在很多场景下也很常见,比如配置项特别多、需要更沉浸式的设置体验时。
但问题也随之而来:如果平台支持的组件越来越多,这里咱们假设是低代码图表场景,如柱状图、折线图、饼图、地图、文本、图片...等等,每个组件都需要一个独立的配置弹窗...🤔,那么,我们应该如何设计一套优雅、可扩展、易维护的代码架构来管理这些层出不穷的 Dialog
呢?🤔
结构设计
万事开头难,尤其是在做一些稍微带点设计或架构意味的事情时,切忌盲目上手。心里得先有个谱,想清楚大致方向,否则等到后面业务需求像潮水般涌来,迭代压力陡增时,你就会深刻体会到早期设计不佳带来的痛苦了(别问小编是怎么知道的...😭)。
当然,如果你已是经验丰富的老司机,那就当我没说哈。😂
面对"组件点击按钮弹出配置框"这个需求,最开始,最直观的想法可能就是:一个组件配一个专属的 Dialog.vue
文件,相互独立,互不影响,挺好不是❓
比如,咱当前有柱状图、折线图、饼图三个组件,那么它们的目录结构可能是这样子的:
咱们不详说其他文件中的代码情况,仅关注每个组件中 Dialog.vue
文件的代码要如何写❓
可能大概是这样:
小编这里使用 Element-Plus 的
el-dialog
组件作为案例演示。
然后,为了在页面上渲染这些不同组件的 Dialog.vue
,最笨的方法可能是在父组件里面用 v-if/v-else-if
来判断, 或者高级一点使用 <component :is="currentDialog">
再配合一堆 import
来动态加载渲染。父组件需要维护哪个弹窗应该显示的状态,以及负责传递数据和接收结果,逻辑很快变得复杂且难以维护。
在项目初期,组件类型少的时候,这种方式确实能跑通,没有问题❗
你就说它能不能跑吧,就算它不能跑,你能跑不就行😋,项目和你总有一个能跑的。
但随着业务不断迭代,支持的组件类型越来越多,这种"各自为战"的模式很快就暴露出了诸多问题,其中有两个问题比较尖锐:
- 缺乏统一控制📝:如果想给所有弹窗统一调整弹窗配置、或者添加一个水印、或者调整一下默认样式、或者增加一个通用的"重置"按钮,怎么办?只能去每个
Dialog.vue
文件里手动修改,效率低下不说,还极易遗漏或出错。 - 代码冗余严重📜:每个
Dialog.vue
文件里,关于弹窗的显示/隐藏逻辑、确认/取消按钮的处理、与 Element Plus (或其他 UI 库) ElDialog 组件的交互代码,几乎都是大同小异的模板代码,写到后面简直是精神污染。(这里手动Q一下我同事🔨)
总之,随着项目的迭代,这种最初看似简单的结构,维护成本越来越高,每次增加或修改一个组件的配置弹窗都成了一种"折磨"。
那么,要如何重新来设计这个架构呢❓
小编采用的是基于动态创建和静态方法关联的架构,其架构的核心理念就是:将通用的弹窗逻辑(创建、销毁、交互)抽离出来,让每个组件的配置面板(Panel)只专注于自身的配置项 UI 视图和数据处理逻辑 ,从而实现高内聚、低耦合、易扩展的目标。
先来瞅瞅目录结构的最终情况👇:
关键变动是 Dialog.vue
变成了 Dialog/index.js
与 Dialog/Panel.vue
,它们俩的作用:
Panel.vue
:负责"长什么样"和"填什么数据" 。index.js
:负责"怎么被调用"和"调用时带什么默认配置",并将Panel.vue
包装后提供给外部使用。
具体实现
接下来,咱们就详细拆解一下这套新架构的设计具体代码实现过程。👇
但为了更好的讲述关键代码的实现,咱们不管拖动那块逻辑,仅通过点击按钮简单的来模拟,效果如下:
本次小编是新建了一个 Vue3 的项目并且安装了 ElementPlus 进行了全局引入,基础项目环境就这样。
然后,从入口出发(App.vue
):
统一管理所有组件导出文件(components/index.js
):
组件入口文件(components/PieChart/index.js
):
该文件用于集中管理组件的核心数据结构与统一的业务逻辑。
咱们以柱状图为例哈。📊
所有组件的基类文件(utils/BaseControl.js
):
该文件是所有组件的"基石"🏛️,每个具体的图表组件都继承自 BaseControl
类,并在该基础上定义自己特有的信息和逻辑。
组件的拖动视图组件(Drag.vue
),这个可以先随便整一个,暂时用不上:
Dialog 组件的入口文件(components/BarChart/Dialog/index.js
):
该文件导入真正的 UI 视图面板(Panel.vue
),然后给组件挂载了一个静态 create
方法。这个 create
方法用于动态创建 Dialog 组件,它内部调用 dialogWithComponent
方法,并可以在此处预设一些该 Dialog 组件特有的配置(如默认标题、宽度)。
Dialog 组件的 Panel.vue
文件:
该组件仅放置柱状图特有的配置信息,并且不需要管弹窗自身的逻辑行为,很干净很专注😎。还有,它内部必须对外提供一个 getValue
方法❗用于在用户点击确认时调用,以获取最终的配置数据。
核心工具函数(utils/dialog.js
)文件 :
dialogWithComponent
这个函数是整个架构的核心!它的职责就像一个专业的 Dialog "召唤师":
脑袋突然蹦出一句话:"去吧,就决定是你了,皮卡丘(柱状图)"🎯
- 动态创建:不再需要在模板里预先写好
<el-dialog>
。dialogWithComponent
会在你需要的时候,通过createApp
和h
函数,动态地创建一个包含<el-dialog>
和你的内容组件的 Vue 应用实例。 - 挂载与销毁:它负责将创建的 Dialog 实例挂载到
document.body
上,并在 Dialog 关闭(确认、取消或点击遮罩层)后,优雅地将其从 DOM 中移除并销毁 Vue 实例,避免内存泄漏。 - Promise 驱动:调用
dialogWithComponent
会返回一个 Promise。当用户点击"确认"并成功获取数据后,Promise 会调用 resolve 并返回数据;如果用户点击"取消"或"关闭",Promise 会调用 reject 。这使得异步处理 Dialog 结果变得异常简洁,并且支持异步。 - 配置注入:你可以轻松地向
dialogWithComponent
传递<el-dialog>
的各种props
,实现 Dialog 的定制化。
createVNode
这个函数是 Vue 中 h
函数的升级版本,它主要是帮忙做内容的渲染🎩,它有两个小小的特点:
- 组件/函数通吃:你可以直接传递一个 Vue 组件 (
.vue
文件或 JS/TS 对象) 给它,它会用h
函数渲染这个组件。你还可以传递一个渲染函数!能让你在运行时动态决定渲染什么内容,简直不要太方便!是吧是吧。🤩 Ref
传递:它巧妙地集中处理了ref
,使得dialogWithComponent
函数可以获取到内容组件的实例 (contentRef.value
),从而能够调用内容组件暴露的方法(getValue
),非常关键的一点。⏰
基础的 Dialog 组件文件(components/BaseDialog.vue
):
那么,整个核心代码的实现过程大概就是如此了。不知道你看完这部分的拆解,是否有了新的收获呢?😋
当然啦,在实际的业务场景中,代码的组织和细节处理会更加复杂,比如会涉及到更精细的状态管理、错误处理、权限控制、以及各种边界情况的兼容等等。这里为了突出咱们动态创建 Dialog 架构的核心思想,小编仅仅是把最关键的脉络拎了出来,并进行了一定程度的精简。
总结
总而言之,言而总之,这次架构的演进,给小编最大的感受就是🏗️从"各自为战"到"统一调度"。
告别了维护繁琐、数量庞大的单个 Dialog.vue 文件,转而拥抱了基于 createApp
和 h
函数的动态创建方式。
这种新模式下,基础 Dialog、配置面板 ( Panel.vue )、以及调用逻辑各司其职,实现了真正的高内聚、低耦合。最终使得整个项目结构更加清晰、代码更加健壮,也极大地提升了后续的可维护性。希望这套方案能给你带来一些启发!
最后,如果你有任何疑问或者更好的想法,随时欢迎交流哦!👇