高性能 v-html 弹窗实现:Vue3 + Element Plus 最佳实践

在现代前端开发中,尤其是低代码平台的场景下,我们常会遇到一个特殊问题:业务要求只能通过 v-html 渲染的内容触发弹窗。这意味着传统的组件引用、模板事件绑定等方式无法直接使用。

一开始,我尝试过两种方式:

  1. 在 DOM 上直接挂载 <el-dialog>
  2. 通过 iframe 引入独立的 Vue 页面

结果发现问题明显:

  • iframe 的渲染性能低,页面打开慢,动画和样式容易错乱
  • 直接挂 DOM 的方式,关闭和销毁逻辑复杂,如果处理不当会导致内存泄漏
  • 同时打开多个弹窗时,需要额外管理容器和组件实例

为了应对这些问题,我整理出了一套 高性能、极简、支持多开、自动销毁的 Vue3 + Element Plus 弹窗方案 ,专门针对 v-html 弹窗触发场景。


方案设计思路

  1. 全局弹窗服务 :dialogModalService.js 负责创建 Vue 实例、挂载组件、传递参数和销毁实例
  2. 子组件只暴露 open 方法:组件自身只负责显示内容,关闭时调用父组件传入的销毁方法
  3. 全局方法挂载 :在 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 弹窗触发场景,性能高、易维护,支持多人协作和扩展。

相关推荐
AI源码3 天前
巨峰影视影院
vue3
RuoyiOffice3 天前
UniApp企业办公:把审批、客户和项目装进手机,能做些什么?
spring boot·vue3·uniapp·crm·flowable·移动办公·企业应用
RuoyiOffice4 天前
SpringBoot OA推荐:面向信创选型,看看这套企业协同平台
spring boot·vue3·信创·flowable·企业协同·ruoyi office·oa选型
周厉4 天前
WebSocket 使用场景与实现方法:Java + Vue3 全栈实战
java·websocket·vue3·websocket双向通讯
红红谈说5 天前
同一套产品两个技术栈,双版本前端工程差异怎么收敛?
前端·uni-app·vue3·前后端分离·双版本维护·接口约定
RuoyiOffice6 天前
Flowable 状态通知设计:单体用本地事件,微服务如何回写业务状态?
spring boot·微服务·vue3·flowable·ruoyi office·redis stream·流程状态通知
AI开源软件6 天前
SpringBoot + Vue3 人力资源管理系统推荐:组织、档案与移动办公
vue3·springboot·ruoyi·oa系统·移动办公·人力资源管理系统·hrm
梅雅达编程笔记6 天前
Day 17 | uni-app 小程序:前端从零搭建
微信小程序·typescript·uni-app·vue3·pinia·前端开发·全栈
RuoyiOffice7 天前
项目预算和交付日期变了,为什么不能直接改台账?
spring boot·项目管理·vue3·flowable·业务建模·ruoyi office·项目变更
AI源码7 天前
图书馆借阅管理系统
vue3·springboot