从声明式到命令式:Vue3 弹窗组件的工厂模式重构

26 阅读2分钟

前言:为什么需要弹窗工厂?

在Vue3项目中,我们经常需要动态创建各种弹窗组件。传统的弹窗实现方式通常需要在父组件中手动管理弹窗的显示/隐藏状态,这种方式存在以下问题:

  • 状态管理复杂:每个弹窗都需要单独的响应式状态
  • DOM操作繁琐:需要手动处理DOM的创建和销毁
  • 代码重复:相似的弹窗逻辑需要重复编写
  • 耦合度高:弹窗组件与父组件紧密耦合

DialogFactory采用工厂模式,提供了一种更加优雅的弹窗解决方案,通过动态创建组件实例,实现了弹窗的解耦和复用。

对比展示:传统方式 vs DialogFactory

传统Element Plus Dialog使用方式

<template>
  <div>
    <el-button @click="dialogVisible = true">打开弹窗</el-button>

    <el-dialog
      v-model="dialogVisible"
      title="基础弹窗"
      width="500px"
    >
      <div>弹窗内容</div>

      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleConfirm">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const dialogVisible = ref(false)

const handleConfirm = () => {
  dialogVisible.value = false
}
</script>

DialogFactory使用方式

<template>
  <el-button @click="openDialog">打开弹窗</el-button>
</template>

<script setup>
import { DialogFactory } from '@/components/MDBaseDialog'
import MyDialog from './MyDialog.vue'

const openDialog = () => {
  DialogFactory(MyDialog)().then(value => {
    console.log('弹窗返回值:', value)
  })
}
</script>

对比总结:

  • DialogFactory将弹窗的创建和管理封装在工厂函数中
  • 不需要在父组件中维护弹窗的显示状态
  • 通过Promise处理弹窗的确认和取消事件
  • 支持动态创建任意弹窗组件

DialogFactory实现原理

核心实现代码

// src/components/MDBaseDialog/index.js
import { createVNode, render } from 'vue'
import { app } from '@/main'

/**
 * 弹窗工厂函数
 * @param {VueComponent} component 弹窗组件
 */
export function DialogFactory(component) {
  return (options = {}) => {
    return new Promise((resolve, reject) => {
      // 创建容器元素
      const container = document.createElement('div')
      const idName = `md-dialog-box-${+new Date()}`
      container.className = 'md-dialog-box'
      container.id = idName

      // 关闭弹窗的处理函数
      const close = () => {
        setTimeout(() => {
          removeDialog()
        }, 300) // 等待动画结束
      }

      // 移除DOM元素
      const removeDialog = () => {
        const dom = document.getElementById(idName)
        dom && document.body.removeChild(dom)
      }

      // 确认按钮处理
      options.confirm = value => {
        resolve(value)
        close()
      }

      // 取消按钮处理
      options.cancel = () => {
        reject('cancel')
        close()
      }

      // 创建组件实例
      const vnode = createVNode(component, options)
      vnode.appContext = app._context

      // 渲染组件到DOM
      render(vnode, container)
      document.body.appendChild(container)
    })
  }
}

关键技术点解析

  1. 动态组件创建:使用createVNode创建虚拟节点
  2. DOM操作:通过render函数将组件渲染到动态创建的容器中
  3. Promise异步处理:使用Promise处理弹窗的确认和取消事件
  4. 自动清理:提供自动移除DOM的机制,避免内存泄漏

使用示例

基础弹窗组件

<!-- MyDialog.vue -->
<template>
  <el-dialog title="基础弹窗" v-model="visible" width="500px" center>
    <div>这是弹窗内容</div>

    <template #footer>
      <el-button @click="cancel">取消</el-button>
      <el-button type="primary" @click="confirm">确定</el-button>
    </template>
  </el-dialog>
</template>

<script setup>
const props = defineProps({
  confirm: {
    type: Function
  },
  cancel: {
    type: Function
  }
})

let visible = $ref(true)

const confirm = () => {
  props.confirm('确认值')
}

const cancel = () => {
  props.cancel()
}
</script>

使用DialogFactory打开弹窗

<template>
  <el-button @click="openDialog">打开弹窗</el-button>
</template>

<script setup>
import { DialogFactory } from '@/components/MDBaseDialog'
import MyDialog from './MyDialog.vue'

const openDialog = () => {
  DialogFactory(MyDialog)().then(value => {
    console.log('用户点击了确定,返回值:', value)
  }).catch(reason => {
    console.log('用户点击了取消:', reason)
  })
}
</script>

带参数的弹窗

<!-- ParameterDialog.vue -->
<template>
  <el-dialog :title="title" v-model="visible" width="600px" center>
    <div>{{ message }}</div>

    <template #footer>
      <el-button @click="cancel">取消</el-button>
      <el-button type="primary" @click="confirm">确定</el-button>
    </template>
  </el-dialog>
</template>

<script setup>
const props = defineProps({
  title: String,
  message: String,
  confirm: Function,
  cancel: Function
})

let visible = $ref(true)
</script>
<template>
  <el-button @click="openParameterDialog">打开带参数的弹窗</el-button>
</template>

<script setup>
import { DialogFactory } from '@/components/MDBaseDialog'
import ParameterDialog from './ParameterDialog.vue'

const openParameterDialog = () => {
  DialogFactory(ParameterDialog)({
    title: '提示',
    message: '这是一个带参数的弹窗'
  }).then(value => {
    console.log('弹窗确认:', value)
  })
}
</script>

复杂表单弹窗

<!-- FormDialog.vue -->
<template>
  <el-dialog title="用户信息" v-model="visible" width="600px" center>
    <el-form :model="formData" label-width="80px">
      <el-form-item label="姓名">
        <el-input v-model="formData.name" />
      </el-form-item>
      <el-form-item label="邮箱">
        <el-input v-model="formData.email" />
      </el-form-item>
    </el-form>

    <template #footer>
      <el-button @click="cancel">取消</el-button>
      <el-button type="primary" @click="confirm">保存</el-button>
    </template>
  </el-dialog>
</template>

<script setup>
const props = defineProps({
  confirm: Function,
  cancel: Function
})

let visible = $ref(true)
const formData = reactive({
  name: '',
  email: ''
})

const confirm = () => {
  props.confirm(formData)
}

const cancel = () => {
  props.cancel()
}
</script>
<template>
  <el-button @click="openFormDialog">打开表单弹窗</el-button>
</template>

<script setup>
import { DialogFactory } from '@/components/MDBaseDialog'
import FormDialog from './FormDialog.vue'

const openFormDialog = () => {
  DialogFactory(FormDialog)().then(formData => {
    console.log('表单数据:', formData)
    // 处理表单数据
  })
}
</script>

总结

DialogFactory通过工厂模式实现了弹窗组件的动态创建和管理,具有以下优势:

  1. 解耦设计:弹窗组件与父组件完全解耦,提高代码复用性
  2. 简化API:使用Promise处理异步操作,代码更加简洁
  3. 自动管理:自动处理DOM的创建和销毁,避免内存泄漏
  4. 灵活扩展:支持任意弹窗组件,易于扩展和维护

这种模式特别适合在大型项目中使用,能够显著提高开发效率和代码质量,是Vue3项目组件化开发的重要实践。