juejin.cn/post/725306... 整篇文章其实就是在这篇文章的基础上做了一点点改进,但是我使用的很舒服,想着分享出来。
Vue 3 命令式弹窗组件
一、核心实现:useCommandComponent.js
javascript
import { createVNode, getCurrentInstance, render } from 'vue'
/**
* 获取弹窗挂载的DOM元素
* 支持三种方式:1.字符串选择器 2.HTMLElement对象 3.默认body
*/
const getAppendToElement = (props) => {
let appendTo = document.body
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
// 字符串选择器,如 "#app" 或 ".container"
appendTo = document.querySelector(props.appendTo)
} else if (props.appendTo instanceof HTMLElement) {
// 直接传入DOM元素
appendTo = props.appendTo
}
// 如果获取失败,回退到body
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body
}
}
return appendTo
}
/**
* 初始化组件实例
* 核心:创建虚拟节点并渲染到DOM
*/
const initInstance = (Component, props, container, appContext = null) => {
// 1. 创建虚拟节点
const vNode = createVNode(Component, props)
// 2. 继承当前组件的上下文(解决provide/inject问题)
vNode.appContext = appContext
// 3. 渲染到临时容器
render(vNode, container)
// 4. 将容器挂载到目标位置
getAppendToElement(props).appendChild(container)
return vNode
}
/**
* 核心Hook:将声明式组件转换为命令式组件
* 特点:简单、灵活、功能完整
*/
export const useCommandComponent = (Component) => {
// 获取当前组件的应用上下文
const appContext = getCurrentInstance()?.appContext
// 关键:继承当前组件的provides链,用于依赖注入
if (appContext) {
const currentProvides = getCurrentInstance()?.provides
// 保持原型链完整
Reflect.set(appContext, 'provides', currentProvides)
}
// 创建临时容器(内存中,尚未挂载到DOM)
const container = document.createElement('div')
/**
* 关闭弹窗并清理资源
* 这个close函数会暴露给外部调用
*/
const close = () => {
// 1. 卸载Vue组件
render(null, container)
// 2. 从DOM中移除容器
container.parentNode?.removeChild(container)
}
/**
* 命令式组件调用函数
* 这是实际被调用的函数,返回一个代理对象
*/
const CommandComponent = (options = {}) => {
// 自动设置visible为true,让弹窗默认显示
if (!Reflect.has(options, 'visible')) {
options.visible = true
}
/**
* 处理关闭回调
* 用户可以通过onClose接收关闭时的参数
* 示例:onClose: (data) => console.log('收到数据:', data)
*/
if (typeof options.onClose !== 'function') {
// 如果没有提供onClose,使用默认关闭函数
options.onClose = close
} else {
// 如果提供了onClose,包裹一层确保能清理DOM
const originOnClose = options.onClose
options.onClose = (...args) => {
// 先执行用户回调,可以传递参数
originOnClose(...args)
// 再执行清理
close()
}
}
// 创建组件实例
const vNode = initInstance(Component, options, container, appContext)
/**
* 返回一个代理对象,提供两种访问方式:
* 1. 通过close属性直接关闭弹窗
* 2. 访问组件通过defineExpose暴露的方法
*/
return new Proxy(vNode, {
// 拦截属性访问
get(target, prop) {
// 1. 支持直接调用close()关闭弹窗
if (prop === 'close') {
return close
}
// 2. 支持访问组件expose的方法
const exposed = target.component?.exposed
if (exposed && Reflect.has(exposed, prop)) {
return Reflect.get(exposed, prop)
}
// 3. 返回原始vNode的属性
return Reflect.get(target, prop)
},
// 拦截in操作符
has(target, prop) {
if (prop === 'close') {
return true
}
const exposed = target.component?.exposed
if (exposed && Reflect.has(exposed, prop)) {
return true
}
return Reflect.has(target, prop)
}
})
}
// 为函数对象本身也添加close方法
CommandComponent.close = close
return CommandComponent
}
export default useCommandComponent
二、弹窗组件示例
1. 通过props打开的弹窗
xml
<!-- BaseDialog.vue -->
<template>
<el-dialog
:model-value="visible"
:title="title"
width="500px"
:before-close="handleBeforeClose"
>
<!-- 内容区域 -->
<div>{{ content }}</div>
<!-- 底部操作按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {defineEmits, defineExpose, defineProps, ref} from 'vue'
// 接收props
const props = defineProps({
visible: { type: Boolean, default: false },
title: { type: String, default: '提示' },
content: { type: String, default: '' },
// 接收父组件传递的数据
payload: { type: Object, default: () => ({}) }
})
// 定义事件
const emit = defineEmits(['close'])
const resultData = ref(null)
// 关闭前处理
const handleBeforeClose = (done) => {
// 可以通过close传递数据
emit('close', {
type: 'cancel',
data: resultData.value,
payload: props.payload
})
done()
}
// 确认操作
const handleConfirm = () => {
resultData.value = { action: 'confirm', time: new Date() }
emit('close', {
type: 'confirm',
data: resultData.value,
payload: props.payload
})
}
// 取消操作
const handleCancel = () => {
resultData.value = { action: 'cancel', time: new Date() }
emit('close', {
type: 'cancel',
data: resultData.value,
payload: props.payload
})
}
// 暴露给外部的方法
const showMessage = (message) => {
console.log('弹窗内部方法被调用:', message)
return `弹窗响应: ${message}`
}
const getDialogData = () => {
return { message: '这是从弹窗获取的数据', time: new Date() }
}
// 定义expose
defineExpose({
showMessage,
getDialogData,
})
</script>
2. 通过expose方法打开的弹窗
xml
<!-- ExposeDialog.vue -->
<template>
<el-dialog
v-model="internalVisible"
:title="params.title"
width="500px"
>
<div>{{ params.content }}</div>
<template #footer>
<el-button @click="internalVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import {reactive, ref} from 'vue'
const emit = defineEmits(['close'])
// 内部状态
const internalVisible = ref(false)
const params = reactive({})
// 通过expose暴露的打开方法
const open = (options = {}) => {
internalVisible.value = true
if (options.title) params.title = options.title
if (options.content) params.content = options.content
return '弹窗已打开'
}
// 自定义方法
const customMethod = () => {
return `自定义方法调用成功,当前数据: ${customData.value}`
}
// 定义expose
defineExpose({
open,
customMethod,
})
</script>
三、关键特性总结
1. 简单到极致
php
// 一步转换
const createDialog = useCommandComponent(MyDialog)
// 一行调用
createDialog({ title: '提示', content: '内容' })
2. 完整的参数传递
php
// 传递数据进去
createDialog({
title: '编辑用户',
content: '修改用户信息',
payload: { userId: 123, userData: {...} }
})
// 接收数据出来
createDialog({
onClose: (result) => {
console.log('用户操作:', result.type) // 'confirm' 或 'cancel'
console.log('返回数据:', result.data)
console.log('传递的数据:', result.payload)
}
})
3. 灵活的expose调用
scss
const dialog = createDialog({...})
// 调用expose的方法
dialog.showMessage('Hello')
dialog.getData()
dialog.customMethod()
// 多种关闭方式
dialog.close({ reason: '用户关闭' }) // 通过hook的close
dialog.closeDialog() // 通过组件expose的closeDialog
四、使用场景对比
传统声明式组件
ini
<template>
<MyDialog
v-model:visible="showDialog"
:title="dialogTitle"
:content="dialogContent"
@close="handleClose"
/>
</template>
<script setup>
// 需要维护多个状态
const showDialog = ref(false)
const dialogTitle = ref('')
const dialogContent = ref('')
// 打开弹窗需要多步操作
const openDialog = () => {
dialogTitle.value = '标题'
dialogContent.value = '内容'
showDialog.value = true
}
</script>
命令式组件
javascript
// 在任何地方,一行代码
const openDialog = () => {
createDialog({
title: '标题',
content: '内容',
onClose: (data) => { /* 处理关闭 */ }
})
}