在现代前端开发中,尤其是低代码平台的场景下,我们常会遇到一个特殊问题:业务要求只能通过 v-html 渲染的内容触发弹窗。这意味着传统的组件引用、模板事件绑定等方式无法直接使用。
一开始,我尝试过两种方式:
- 在 DOM 上直接挂载
<el-dialog> - 通过 iframe 引入独立的 Vue 页面
结果发现问题明显:
- iframe 的渲染性能低,页面打开慢,动画和样式容易错乱
- 直接挂 DOM 的方式,关闭和销毁逻辑复杂,如果处理不当会导致内存泄漏
- 同时打开多个弹窗时,需要额外管理容器和组件实例
为了应对这些问题,我整理出了一套 高性能、极简、支持多开、自动销毁的 Vue3 + Element Plus 弹窗方案 ,专门针对 v-html 弹窗触发场景。
方案设计思路
- 全局弹窗服务 :
dialogModalService.js负责创建 Vue 实例、挂载组件、传递参数和销毁实例 - 子组件只暴露
open方法:组件自身只负责显示内容,关闭时调用父组件传入的销毁方法 - 全局方法挂载 :在
main.js将openGlobalDialogModal挂在window上,v-html 可直接调用
核心原则:点击 v-html 渲染的按钮 → 调用全局方法 → 服务创建独立容器挂载组件 → 关闭时自动销毁。
1️⃣ 弹窗服务 dialogModalService.js
javascript
// src/utils/dialogModalService.js
import { createVNode, render } from 'vue'
import FundPlanSummaryDialog from '@/pages/office/fund/fundPlanSummary/fundPlanSummaryDialog.vue'
// 弹窗注册表
const modalRegistry = {
fundPlanSummaryDialog: FundPlanSummaryDialog
}
// 保存弹窗容器列表
const dialogList = []
let globalApp = null
/**
* 初始化弹窗服务(仅需调用一次)
* @param {App} app - Vue 主应用实例
*/
export function initDialogModalService(app) {
globalApp = app
window.openGlobalDialogModal = openGlobalDialogModal
}
export function openGlobalDialogModal(name, params = {}) {
if (!globalApp) {
console.warn('[dialogModalService] 请先 initDialogModalService(app)')
return
}
const DialogComponent = modalRegistry[name]
if (!DialogComponent) {
console.warn('[dialogModalService] 弹窗未注册:', name)
return
}
// 创建独立容器
const container = document.createElement('div')
document.body.appendChild(container)
dialogList.push(container)
const vnode = createVNode(DialogComponent, {
params,
onDestroy: () => {
render(null, container)
container.remove()
const idx = dialogList.indexOf(container)
if (idx > -1) dialogList.splice(idx, 1)
}
})
// 复用主 app 的上下文(el-dialog 全局组件、i18n 等插件都能正常工作)
vnode.appContext = globalApp._context
render(vnode, container)
// 调用弹窗组件暴露的 open 方法
vnode.component?.exposed?.open?.(params)
}
2️⃣ main.js 初始化弹窗服务
javascript
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { initDialogModalService } from '@/utils/dialogModalService.js'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import i18n from '@/assets/lang'
const app = createApp(App)
app.use(ElementPlus, { locale: zhCn })
app.use(i18n)
// 初始化弹窗服务(传入主 app 实例)
initDialogModalService(app)
app.mount('#app')
3️⃣ 弹窗组件 FundPlanSummaryDialog.vue
必须执行 props.onDestroy?.(),保证销毁
vue
<template>
<el-dialog
v-model="dialogVisible"
title="资金计划明细"
width="90vw"
:destroy-on-close="true"
draggable
top="10px">
<div class="dialog-content">
<fund-plan-summary-index
v-if="dialogVisible"
:plan-month="planMonth"
:show-toolbar-buttons="false"/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="closeDialog">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
onDestroy: { type: Function, required: true }
})
const dialogVisible = ref(false)
const planMonth = ref('')
const open = (params = {}) => {
planMonth.value = params.planMonth || ''
dialogVisible.value = true
}
const closeDialog = () => {
dialogVisible.value = false
// 必须执行,保证销毁
props.onDestroy?.()
}
defineExpose({ open })
</script>
4️⃣ 页面 v-html 调用示例
vue
<template>
<div v-html="htmlStr"></div>
</template>
<script setup>
import { ref } from 'vue'
const htmlStr = ref(`
<button onclick="openGlobalDialogModal('fundPlanSummaryDialog', {planMonth:'2024-01'})">
打开资金计划弹窗
</button>
`)
</script>
v-html 调用示例:
vue
<template>
<div v-html="htmlStr"></div>
</template>
<script setup>
import { ref } from 'vue'
import { getFundPlanSummaryButton } from '@/api/office/fund/fundPlanSummary'
const htmlStr = ref('')
// 模拟从后端获取 HTML 字符串
const loadHtml = async () => {
const res = await getFundPlanSummaryButton({ planMonth: '2024-01' })
htmlStr.value = res.data // 后端返回带 onclick 的 HTML
}
loadHtml()
</script>
优势与总结
- 复用主 app 上下文 :使用
vnode.appContext = globalApp._context,无需创建独立 app,el-dialog 全局组件、i18n 等插件都能正常工作 - 多弹窗支持:每个弹窗独立容器,互不干扰
- 高性能:不使用 iframe,不直接挂 DOM
- v-html 可直接触发
- 安全稳定:点击关闭立即卸载实例,防止内存泄漏
- 极简实现:无需管理独立 app 的插件安装和全局属性赋值
该方案适用于 v-html 弹窗触发场景,性能高、易维护,支持多人协作和扩展。