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

1. 快速开始

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

javascript 复制代码
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):

xml 复制代码
<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>

调用方式:

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

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

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

组件定义 (WizardDialog.vue):

xml 复制代码
<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>

调用方式:

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

3. 常用配置项

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

4. 核心源码实现

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

javascript 复制代码
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. 源码分段解析

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

第一部分:上下文准备

ini 复制代码
const instance = getCurrentInstance()
let appContext = null
if (instance) {
  appContext = {...instance.appContext}
  appContext.provides = instance.provides
}

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

第二部分:DOM 挂载与清理

scss 复制代码
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 塞进页面。关闭时则反向操作,彻底销毁组件。

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

javascript 复制代码
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 污染与关闭动画修复

相关推荐
天才熊猫君2 小时前
📄 第二篇:Vue 3 命令式弹窗 provide/inject 机制解析
前端·javascript·vue.js
iReachers2 小时前
HTML打包EXE工具数据加密功能详解 - 加密保护HTML/JS/CSS资源
javascript·css·html·html加密·html转exe·html一键打包exe·exe打包
kyriewen112 小时前
代码写成一锅粥?这5种设计模式让你的项目“起死回生”
前端·javascript·设计模式·typescript·ecmascript·html5
ywlovecjy2 小时前
【Nginx 】Nginx 部署前端 vue 项目
前端·vue.js·nginx
skywalk81632 小时前
g4f JavaScript调用报错问题解决
开发语言·javascript·ecmascript
Alice-YUE2 小时前
AI对话为什么需要RAG
前端·语言模型·rag
C澒2 小时前
IntelliPro 企业级产研协作平台:低代码实时预览与可视化编辑技术调研
前端·低代码·ai编程
霍理迪2 小时前
TS类型断言和类型守卫
前端