
本文详解如何在 Vue 3 中替代 Vue 2 的 new Vue({ extends: Component }) 模式,通过 createApp + 动态挂载 + expose API 实现可编程调用的 Modal(如 showPrompt),解决 $on、$destroy、$mount 等 Vue 2 实例方法不可用的问题。
本文详解如何在 vue 3 中替代 vue 2 的 new vue({ extends: component }) 模式,通过 createapp + 动态挂载 + expose api 实现可编程调用的 modal(如 showprompt),解决 $on、$destroy、$mount 等 vue 2 实例方法不可用的问题。
在 Vue 3 中,组件不再是一个可直接 new 的构造函数,Vue.component() 仅用于全局注册,不返回实例;同时,$on、$destroy、$mount 等 Vue 2 实例方法已被彻底移除。要实现类似 showPrompt() 这类“程序化弹窗”能力(即运行时动态创建、传参、监听事件、手动销毁),必须采用 Vue 3 原生推荐的组合式 API 方案:动态创建应用实例 + defineExpose 显式暴露控制接口 + 手动 DOM 挂载与卸载。
以下是安全、可维护、符合 Vue 3 设计哲学的重构方案:
✅ 正确做法:使用 createApp + defineExpose + 手动生命周期管理
首先,确保你的 Modal.vue 显式暴露关键方法和状态:
<!-- @/layouts/Modal.vue -->
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
const props = defineProps({
positiveButtonColor: { type: String, default: 'error' },
showPositiveButton: { type: Boolean, default: true },
contentSlotPaddingClass: { type: String, default: 'pa-0' },
// ...其他 props
})
const emit = defineEmits(['dialogObserverCallback', 'confirm', 'cancel'])
const isVisible = ref(false)
const isMounted = ref(false)
// 对外暴露的控制 API(必须通过 defineExpose)
const show = () => {
isVisible.value = true
}
const hide = () => {
isVisible.value = false
}
const destroy = () => {
// 触发销毁前回调
emit('dialogObserverCallback', false)
// 清理 DOM(由外部调用方负责)
}
defineExpose({
show,
hide,
destroy,
isVisible, // 可选:暴露响应式状态供外部读取
// 注意:不要暴露 props 或内部 ref(如 isMounted),避免破坏响应式边界
})
</script>
<template>
<v-dialog v-model="isVisible" persistent max-width="500">
<v-card>
<v-card-text>{{ $slots.default?.() }}</v-card-text>
<v-card-actions>
<v-btn v-if="showPositiveButton" color="primary" @click="$emit('confirm')">
{{ positiveButtonText || 'OK' }}
</v-btn>
<v-btn text @click="$emit('cancel')">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>然后,重写 showPrompt 函数,完全脱离 Vue 2 实例思维:
立即学习“前端免费学习笔记(深入)”;
// utils/modal-prompt.ts
import { createApp, VNode, Teleport, h } from 'vue'
import Modal from '@/layouts/Modal.vue'
// 全局容器:用于挂载临时 modal(建议在 App 根节点下预设 <div id="modal-container"></div>)
const container = document.getElementById('modal-container') || (() => {
const el = document.createElement('div')
el.id = 'modal-container'
document.body.appendChild(el)
return el
})()
export function showPrompt(props: Record<string, any>): {
instance: ReturnType<typeof createApp>['_instance']
unmount: () => void
expose: Awaited<ReturnType<typeof createApp>['exposed']>
} {
// 1. 合并默认 props
const finalProps = {
positiveButtonColor: 'error',
showPositiveButton: true,
contentSlotPaddingClass: 'pa-0',
...props
}
// 2. 若需动态修改按钮文案逻辑(原代码中的 set/get),在此处理:
if (finalProps.positiveButtonText === 'Yes') {
finalProps.positiveButtonColor = 'primary'
}
// 3. 创建独立应用实例(注意:不继承全局 app 上下文!)
const app = createApp({
setup() {
return () => h(Modal, finalProps, {
default: () => props.default?.() || []
})
}
})
// 4. 挂载到容器
const vm = app.mount(container)
// 5. 获取暴露的 API(需等待 nextTick 或 useSlots 后可用,此处简化为同步访问)
// ⚠️ 注意:exposed 是响应式代理,需 await 或 nextTick 才能确保 ready,但多数场景可直接访问
const exposed = vm.exposed as typeof vm.exposed & {
show: () => void
hide: () => void
destroy: () => void
}
// 6. 自动显示
exposed.show()
// 7. 返回可控句柄
return {
instance: vm,
unmount: () => {
// 卸载前触发隐藏 + 销毁回调
exposed.hide()
setTimeout(() => {
app.unmount()
// 清理 DOM 节点(可选:若 container 仅用于 modal,可保留)
if (container.firstChild) container.innerHTML = ''
}, 1000)
},
expose: exposed
}
}
// 使用示例(在 setup 或 Composition API 中):
// const { unmount, expose } = showPrompt({ title: '确认操作' })
// expose.hide() // 手动关闭
// unmount() // 彻底销毁⚠️ 关键注意事项
插件上下文丢失:
createApp()创建的实例不会自动继承父应用的vuetify、i18n、router等插件。若 Modal 依赖这些,需显式app.use(vuetify).use(i18n)—— 但更推荐将 Modal 改为普通<teleport></teleport>弹窗 +v-model控制,避免动态实例化。-
推荐替代方案(生产首选):
对于高频使用的 Prompt/Modal,应改为声明式 +v-model+Teleport:<Teleport to="#modal-container"> <Modal v-model="promptVisible" v-bind="promptProps" @confirm="onConfirm" /> </Teleport>
配合
const promptVisible = ref(false)和const promptProps = reactive({...}),语义清晰、调试友好、无内存泄漏风险。 类型安全增强:为
showPrompt返回值添加精确类型定义,利用InstanceType<typeof modal></typeof>(需.d.ts补全)或ReturnType<typeof defineexpose></typeof>提升 TS 支持。内存清理必须手动:Vue 3 不再提供
$destroy,务必调用app.unmount()并清空容器 DOM,否则导致内存泄漏。
此方案既尊重 Vue 3 的响应式设计范式,又满足程序化调用的真实需求,是当前生态中最健壮、可长期维护的实践路径。


















