Vue3集成Gridstack.js时网格项内嵌Vue组件响应式失效问题
问题根源
响应式失效的核心原因非常明确:你传入addWidget的content是div.outerHTML——这只是一份静态HTML字符串快照,和你之前执行app.mount(div)生成的响应式Vue DOM实例没有任何关联。相当于你先把Vue组件渲染到了一个不在页面DOM树里的临时div上,转头只复制了这个div的静态HTML代码塞给Gridstack,真正带响应式能力的DOM节点根本没被插到页面里。后续你再调用app.mount(div.id)的时候,Gridstack还没把对应id的节点渲染到页面上,挂载操作完全不生效,最终页面上显示的只是没有任何响应式绑定的静态HTML,自然不会更新。
实现方案
核心思路是放弃给Gridstack传静态HTML字符串的逻辑,等Gridstack创建好真实的页面DOM节点后,直接把Vue组件挂载到节点内置的.grid-stack-item-content容器上,就能完整保留Vue的响应式能力。具体实现要点:
- 初始化Gridstack时不提前传入内容,避免静态HTML覆盖后续挂载的Vue节点
- 新增/初始化网格项时,先让Gridstack生成真实DOM,再执行Vue挂载操作
- 统一维护所有创建的Vue应用实例,在网格项删除、页面卸载时主动执行卸载逻辑,避免内存泄漏
- 用Vue的
h()渲染函数挂载组件、传递props,比extends扩展组件的方式更稳定,不会破坏组件原有的生命周期和响应式逻辑
修正后的完整代码
<template> <div class="p-6 h-full flex flex-col"> <header class="flex flex-row items-center justify-between mb-6"> <div> <h1 class="text-3xl font-bold"> Workbench </h1> <p class="leading-6 text-gray-600 text-sm mt-2"> {{ info }} </p> </div> <div class="flex flex-row items-center"> <button type="button" @click="addPanel()">Add Panel</button> </div> </header> <div class="flex-1"> <section class="grid-stack"></section> </div> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, createApp, h } from "vue" import TestPanel from "./../components/panels/TestPanel.vue" let grid = null; // 存储网格节点和对应Vue实例的映射关系,用于清理 const appInstances = new Map() const items = [ { x: 0, y: 0, h: 4, w: 6, comp: TestPanel, props: { title: "面板1" } }, { x: 7, y: 0, h: 4, w: 6, comp: TestPanel, props: { title: "面板2" } }, { x: 0, y: 5, h: 4, w: 4, comp: TestPanel, props: { title: "面板3" } }, { x: 4, y: 5, h: 4, w: 4, comp: TestPanel, props: { title: "面板4" } }, { x: 8, y: 5, h: 4, w: 4, comp: TestPanel, props: { title: "面板5" } }, ]; /** * 把Vue组件挂载到指定Gridstack网格项上 * @param {HTMLElement} widgetEl Gridstack生成的网格项节点 * @param {Component} component 要挂载的Vue组件 * @param {Object} props 传给组件的属性 */ function mountVueToWidget(widgetEl, component, props = {}) { const contentContainer = widgetEl.querySelector('.grid-stack-item-content') contentContainer.innerHTML = '' const app = createApp({ render: () => h(component, props) }) app.mount(contentContainer) appInstances.set(widgetEl, app) } onMounted(() => { grid = GridStack.init({ cellHeight: "70px", minRow: 1, }); // 监听网格项删除事件,自动卸载对应Vue实例 grid.on('removed', (_, removedItems) => { removedItems.forEach(item => { const app = appInstances.get(item.el) if (app) { app.unmount() appInstances.delete(item.el) } }) }) // 初始化加载默认网格项 items.forEach(itemConfig => { const { comp, props, ...gridOption } = itemConfig const widget = grid.addWidget(gridOption) mountVueToWidget(widget, comp, props) }) }); onBeforeUnmount(() => { // 页面销毁时清理所有实例和Gridstack appInstances.forEach(app => app.unmount()) appInstances.clear() grid.destroy(false) }) function addPanel() { // 先生成空的网格项 const widget = grid.addWidget({ x: 0, y: 0, w: 6, h: 3, }) // 挂载Vue组件,直接传ref/reactive类型的props即可保持响应式 mountVueToWidget(widget, TestPanel, { test: "this is a test" }) } </script> <style> .grid-stack-item-content { background-color: #18BC9C; overflow: hidden; /* 避免组件内容溢出网格项边界 */ } </style>
优化提示
- 如果需要实现网格项缩放后的组件自适应,可以监听Gridstack的
resizestop事件,把最新的宽高值通过props传给对应组件,组件内部监听尺寸变化做响应式适配即可 - 传递props时直接传入
ref、reactive包裹的响应式变量,Vue会自动保持响应式绑定,不需要额外处理 - 如果需要在网格项里支持Vue的全局插件、全局属性,可以把
createApp的逻辑抽成公共方法,统一注册插件后再挂载,和普通Vue应用的使用逻辑完全一致
内容的提问来源于stack exchange,提问作者Nick Verheijen
相关产品推荐
相关产品推荐

