vue3封装弹窗组件
遇到的问题
- 不能用vue2中的new Vue(Dialog) 要用vue3中的createNode()+render
- 创建出来的组件是野生的,独立的,不属于任何app,拿不到全局注册的elementuiPlus组件
- 想要用app注册的全局组件,需要给创建的弹窗组件指定上下文(appContext)
- 也不能直接将整个 app 实例 挂载到了弹窗容器上,导致 Vue 认为你要在同一个 div 上挂载两个根应用。
- 要获取上下文 getCurrentInstance,import{ getCurrentInstance }from'vue',
- 而getCurrentInstance() 只能在 setup 内部运行,在普通 JS 函数里运行就是 null!
- 所以最好在APP.vue 中全局获取,然后传给dialog, dialog.appContext = appContext // 传递 appContext 以支持全局组件和插件
传值,使用异步函数resolve出来,
封装的函数 里定义函数。去传给封装的组件,组件在调用函数时传值给他
总结
- vue3中创建的应用是独立,野生的,也就是具备应用隔离,那么也很适合微前端开发
- Vue2 是全局单例架构,所有 Vue 实例共享同一个全局上下文,因此天然不需要手动传递上下文;Vue3 是应用隔离架构,每个 createApp 都是独立实例,上下文不共享,手动渲染的组件必须显式指定 appContext 才能继承全局插件
1. 整体架构介绍
- “实现了一个函数式的弹窗系统,核心是 popup 方法,它基于 Vue 3 的 createVNode 和 render 动态渲染弹窗组件,并返回一个 Promise 来处理异步结果。”
2. 返回值获取机制 - “popup 方法返回一个 Promise 对象,调用方可以通过 await 或 .then() 来获取弹窗的用户操作结果。”
- 示例代码:
3. 内部实现细节 - “在 popup 内部,我创建了一个 Promise,并在 createVNode 时传递 onResult 回调给 Dialog 组件。”
- “当用户在弹窗中点击按钮(如确认或取消)时,Dialog 组件会调用 prop.onResult(data),传递操作数据(如 { action: 'confirm' })。”
- “onResult 回调接收数据后,执行清理 DOM 的逻辑,然后 resolve(data) 解析 Promise,让调用方拿到返回值。”
4. 优势和设计考虑 - “这种设计让弹窗调用像同步函数一样简单,支持 await,同时内部处理了 DOM 清理和错误边界,避免内存泄漏。”
- “如果弹窗被关闭而没有操作,会通过 onClose 回调 resolve(null),确保 Promise 总是完成。”
5. 潜在问题和优化 - “在实现中,添加了参数验证和错误处理,确保调用安全。如果组件加载失败,会 reject Promise。”
- “为了支持 Element Plus 等全局组件,传递了 appContext。”
Dialog.vue
<template>
<div class="openHmtl">
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width"
:before-close="handleClose"
class="tanchuan"
>
<component :is="page" :param="param" @confirm="onPageOk" @cancel="onPageCancel"
@disabled="disabledButton" @setButtons="getButtons"/>
<template #footer v-if="buttons && buttons.length > 0">
<div class="dialog-footer">
<el-button v-for="button of buttons" @click="buttonClicked(button)" :key="button.name" :type="button.name === '确认' ? 'primary' : 'default'"
:disabled="button.disabled ? button.disabled : false" style="margin-right: 10px;">{{ button.name
}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const prop = defineProps({
title: {
type: String,
default: '标题'
},
param: {
type: Object,
default: () => ({})
},
page: {
type: [Object, Function],
default: ''
},
// 兼容 onClose 事件回调
onClose: Function,
onResult: Function
})
const width = ref('40%')
const buttons = ref([])
const dialogVisible = ref(true)
const getButtons = (btns) => {
if (!Array.isArray(btns)) {
console.warn('Dialog: setButtons payload must be an array', btns)
return
}
buttons.value = btns.map((button) => ({
...button,
disabled: button.disabled || false
}))
}
const handleClose = () => {
if (typeof prop.onClose === 'function') {
prop.onClose()
}
}
const onPageOk = (data) => {
if (typeof prop.onResult === 'function') {
prop.onResult(data || { action: 'confirm' })
}
dialogVisible.value = false
}
const onPageCancel = () => {
if (typeof prop.onResult === 'function') {
prop.onResult({ action: 'cancel' })
}
if (typeof prop.onClose === 'function') {
prop.onClose()
}
dialogVisible.value = false
}
const disabledButton = (buttonName, disabled) => {
const button = buttons.value.find((btn) => btn.name === buttonName)
if (button) {
button.disabled = disabled
}
}
const buttonClicked = (button) => {
if (typeof button.click === 'function') {
button.click()
}
dialogVisible.value = false
}
</script>
<style scoped lang="scss">
.el-dialog {
border-radius: 5px;
position: absolute;
top: 50%;
left: 50%;
margin: 0 !important;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
}
.tanchuan :deep(.el-dialog__body){
border-bottom: 1px solid gainsboro;
border-top: 1px solid gainsboro;
overflow-y: scroll;
height: 80%;
max-height: 500px;
}
.openHmtl :deep(.el-dialog__header) {
font-weight: 600;
font-size: 18px;
color: #D3602B;
}
</style>
utlis.js
// 全局存储 appContext(只赋值一次)
let appContext = null
export function setDialogContext(ctx) {
appContext = ctx
}
/**
* 弹窗函数调用(Vue3标准写法,返回Promise)
* @param {string} title 弹窗标题
* @param {string} path 组件路径(views下的文件名,不带.vue)
* @param {object} param 传递给组件的参数
* @param {number|string} [width] 弹窗宽度(百分比或像素,默认40%)
* @returns {Promise} 返回弹窗结果的Promise
*/
popup(title, path, param = {}, width = '40%') {
return new Promise((resolve, reject) => {
if (!title || !path) {
reject(new Error('popup: title and path are required'))
return
}
const container = document.createElement('div')
// 不设置 pointerEvents,保证 el-dialog 能正常交互
const cleanup = () => {
render(null, container)
if (document.body.contains(container)) {
document.body.removeChild(container)
}
}
let page
try {
page = getAsyncComponent(path)
if (!page) {
throw new Error(`popup: component ${path} not found`)
}
} catch (error) {
reject(error)
return
}
try {
document.body.appendChild(container)
const dialog = createVNode(Dialog, {
title,
page,
param,
width,
onClose: () => {
cleanup()
},
onResult: (data) => {
cleanup()
resolve(data)
},
})
dialog.appContext = appContext // 传递 appContext 以支持全局组件和插件
render(dialog, container)
} catch (error) {
console.error('popup: failed to render dialog', error)
cleanup()
reject(error)
}
})
},
App.vue
onMounted(() => {
// 全局存一次,永久生效
const { appContext } = getCurrentInstance()
setDialogContext(appContext)
})
a.vue
<el-button
type="primary"
@click="freestyle"
>享受自由人生</el-button>
const freestyle = () => {
utils.popup('享受自由人生', 'freestyle');
};

浙公网安备 33010602011771号