📄 第一篇:Vue 3 命令式弹窗使用指南

7 阅读4分钟

1. 快速开始

通过 useCommandComponent,你可以像调用函数一样打开一个弹窗,而无需在模板中写 <Dialog /> 标签。

import { useCommandComponent } from './composables/useCommandComponent'
import MyDialog from './components/MyDialog.vue'// 1. 创建弹窗构造函数
const showDialog = useCommandComponent(MyDialog)
​
// 2. 调用函数打开弹窗
showDialog({
  title: '提示',
  content: '这是一个命令式弹窗',
  onClosed: (result) => {
    console.log('弹窗关闭,返回结果:', result)
  }
})

2. 两种核心用法

模式 A:Props 驱动(推荐简单场景)

适用于表单提交、确认框等一次性交互。你只需要传入参数并监听关闭回调。

组件定义 (ConfirmDialog.vue):

<template>
  <el-dialog :model-value="visible" :title="title" @closed="handleClosed">
    <p>{{ content }}</p>
    <template #footer>
      <el-button @click="handleCancel">取消</el-button>
      <el-button type="primary" @click="handleConfirm">确定</el-button>
    </template>
  </el-dialog>
</template><script setup>
defineProps(['visible', 'title', 'content'])
const emit = defineEmits(['closed'])
​
const handleConfirm = () => emit('closed', { action: 'confirm' })
const handleCancel = () => emit('closed', { action: 'cancel' })
const handleClosed = () => emit('closed', { action: 'close' })
</script>

调用方式:

const ConfirmDialog = useCommandComponent(ConfirmDialog)
​
ConfirmDialog({
  title: '删除确认',
  content: '确定要删除这条数据吗?',
  onClosed: (res) => {
    if (res.action === 'confirm') deleteItem()
  }
})

模式 B:Expose 驱动(推荐复杂交互)

适用于多步骤向导、需要外部触发更新或获取内部状态的复杂弹窗。

组件定义 (WizardDialog.vue):

<template>
  <el-dialog v-model="internalVisible" title="向导">
    <div>当前步骤: {{ step }}</div>
  </el-dialog>
</template><script setup>
import { ref } from 'vue'
const internalVisible = ref(false)
const step = ref(1)
​
// 暴露方法给外部调用
const open = (options) => {
  internalVisible.value = true
  step.value = options.startStep || 1
}
​
defineExpose({ open })
</script>

调用方式:

const WizardDialog = useCommandComponent(WizardDialog)
​
const dialogInstance = WizardDialog() // 此时弹窗未显示
dialogInstance.open({ startStep: 2 }) // 手动控制打开并传参

3. 常用配置项

属性类型说明
visibleBoolean默认为 true,控制弹窗显隐
appendToString/HTMLElement挂载点,默认为 body
onClosedFunction弹窗完全关闭(动画结束)后的回调

4. 核心源码实现

你可以直接将以下代码保存为 useCommandComponent.js。它封装了 Vue 3 的底层渲染逻辑,支持自动挂载、上下文传递以及实例方法暴露。

import { createVNode, getCurrentInstance, render } from 'vue'export const useCommandComponent = (Component) => {
  // 1. 获取当前实例,提取 appContext 和 provides
  const instance = getCurrentInstance()
  let appContext = null
​
  if (instance) {
    // 创建一个关联的上下文对象
    // 目的:把父组件的 provides 传进去,否则动态渲染的弹窗会读不到父级数据
    appContext = {...instance.appContext}
    appContext.provides = instance.provides
  }
​
  // 2. 确定弹窗应该挂载到哪个 DOM 节点(默认是 body)
  const getAppendToElement = (props) => {
    let appendTo = document.body
    if (props.appendTo) {
      if (typeof props.appendTo === 'string') appendTo = document.querySelector(props.appendTo)
      else if (props.appendTo instanceof HTMLElement) appendTo = props.appendTo
      if (!(appendTo instanceof HTMLElement)) appendTo = document.body
    }
    return appendTo
  }
​
  // 3. 创建虚拟节点并渲染到临时容器中
  const initInstance = (Component, props, container, appContext = null) => {
    const vNode = createVNode(Component, props)
    vNode.appContext = appContext // 将准备好的上下文传给组件
    render(vNode, container)
    getAppendToElement(props).appendChild(container)
    return vNode
  }
​
  const container = document.createElement('div')
​
  // 4. 关闭函数:卸载组件实例并从 DOM 中移除容器
  const closed = () => {
    render(null, container)
    container.parentNode?.removeChild(container)
  }
​
  const CommandComponent = (options = {}) => {
    // 默认设置弹窗为显示状态
    if (!Reflect.has(options, 'visible')) options.visible = true
​
    // 包装 onClosed 回调:确保动画结束后再执行 DOM 清理
    if (typeof options.onClosed !== 'function') {
      options.onClosed = closed
    } else {
      const originOnClosed = options.onClosed
      options.onClosed = (...args) => {
        originOnClosed(...args)
        closed()
      }
    }
​
    const vNode = initInstance(Component, options, container, appContext)
​
    // 5. 返回一个代理对象,实现对组件实例的灵活控制
    return new Proxy(vNode, {
      get(target, prop) {
        if (prop === 'closed') return closed // 允许外部调用 .closed()
        const exposed = target.component?.exposed
        if (exposed && Reflect.has(exposed, prop)) {
          return Reflect.get(exposed, prop) // 允许访问 defineExpose 暴露的方法
        }
        return Reflect.get(target, prop)
      },
      has(target, prop) {
        if (prop === 'closed') return true
        const exposed = target.component?.exposed
        if (exposed && Reflect.has(exposed, prop)) return true
        return Reflect.has(target, prop)
      }
    })
  }
​
  CommandComponent.closed = closed
  return CommandComponent
}

5. 源码分段解析

为了让你更清楚每一块代码的作用,我们将上面的源码拆分为三个核心部分:

第一部分:上下文准备

const instance = getCurrentInstance()
let appContext = null
if (instance) {
  appContext = {...instance.appContext}
  appContext.provides = instance.provides
}

说明: 这一步是为了拿到父组件的 provides。因为弹窗是动态创建的,如果不手动把父级的数据传给它,弹窗里的 inject 就会失效。

第二部分:DOM 挂载与清理

const initInstance = (Component, props, container, appContext = null) => {
  const vNode = createVNode(Component, props)
  vNode.appContext = appContext
  render(vNode, container)
  getAppendToElement(props).appendChild(container)
  return vNode
}
​
const closed = () => {
  render(null, container)
  container.parentNode?.removeChild(container)
}

说明: 这里利用 createVNoderender 手动把组件渲染到一个临时的 div 里,然后把这个 div 塞进页面。关闭时则反向操作,彻底销毁组件。

第三部分:代理与交互控制

return new Proxy(vNode, {
  get(target, prop) {
    if (prop === 'closed') return closed
    const exposed = target.component?.exposed
    if (exposed && Reflect.has(exposed, prop)) return Reflect.get(exposed, prop)
    return Reflect.get(target, prop)
  },
  // ... has 拦截器
})

说明: 使用 Proxy 是为了让返回值既能当普通对象用(访问 expose 的方法),又能直接调用 .closed() 来关闭弹窗,使用起来非常灵活。

📄 第一篇:Vue 3 命令式弹窗使用指南

📄 第二篇:Vue 3 命令式弹窗 provide/inject 机制解析

📄 第三篇:Vue 3 命令式弹窗 Provide 污染与关闭动画修复