首页 / 资讯中心 / 文章详情

Vue iframe缓存最佳实践:常驻DOM + v-show实现状态不丢失

Vue iframe缓存最佳实践:常驻DOM + v-show实现状态不丢失 ★ FEATURED ARTICLE
1. 先说清楚问题iframe到底卡在哪儿1.1 那种让人抓狂的“一切换就重来”做中后台开发和数据大屏集成的小伙伴对下面这个场景应该不陌生业务系统里需要嵌入第三方页面比如某个地图大屏、某个BI报表、某个流程设计器甚至是内部的老系统。为了方便大家直接在路由组件里写一个iframesrc指向第三方地址。刚开始觉得简单省事但一旦在几个菜单之间来回切换问题就暴露了每次切走再切回来iframe都会白屏加载一次内部的输入内容、滚动位置、视频播放进度、报表筛选条件全部回到初始状态。用户就会来问“怎么切回来就变成重新打开了”这个问题的本质就是“iframe刷新”。在Vue项目里大家第一个想到的解决办法通常是keep-alive。毕竟keep-alive是官方提供的组件缓存方案很多人天真地认为只要给router-view包一层keep-alive页面就不会重新挂载了。但实际操作下来发现iframe内部的页面还是照刷不误。我在某公司的一个中后台项目里就踩过这个坑当时给iframe宿主组件加了keep-alive结果切走再切回来第三方系统直接重新加载之前填写的所有表单数据全没了。最后排查了半天问题不在缓存配置而是对iframe的“可缓存边界”理解有误。1.2 keep-alive缓存的边界到底在哪要搞懂这个问题需要先想清楚keep-alive缓存了什么。keep-alive缓存的是Vue组件实例包括组件的响应式数据、props、子组件树以及对应的VNode。组件被缓存后下次激活不会再走mounted而是走activated。但iframe不是一个Vue组件它是浏览器内置的原生元素。iframe内部的文档、脚本、会话由浏览器内核直接管理Vue的响应式系统根本管不到。iframe的加载、刷新、销毁只跟一个事情有关iframe这个DOM节点在文档树中的生命周期。只要iframe节点从文档中被移除再插入回文档浏览器就会给它重新导航一次。这里有个很容易踩的细节keep-alive虽然不会让组件走unmounted但对于那些超出缓存数量max属性的组件keep-alive会按LRU策略把旧组件移除缓存这时组件会真正卸载只要iframe节点被卸载再次进入页面必然重新加载。另外即使组件被缓存keep-alive在切换时往往会把组件移动到隐藏容器中对某些第三方页面来说这种离开可视区域、脱离文档流的行为会触发各种幺蛾子例如基于IntersectionObserver的懒加载直接失效上下文里的postMessage通信中断。所以keep-alive从来不是缓存iframe的首选方案。如果打个比方来理解keep-alive等于帮你把正在编辑的文档保存到抽屉里切换页面只是收起文档而iframe是你在另一个窗口里打开的真实网页那个网页进程是否存活取决于浏览器是否还持有这条iframe连接。只要iframe的DOM从文档里拔掉那个网页就会和这个窗口一起被关闭。因此解决iframe不刷新问题的真正关键不是“缓存组件状态”而是“保证iframe节点始终存在于文档树中”。2. 最容易落地的核心方案iframe常驻 v-show切换2.1 把iframe从路由出口中解放出来既然要保证iframe节点始终存在最直接的办法就是不要让路由去控制iframe的创建和销毁。我们需要把iframe放到一个“永远不卸载”的容器中用最简单的方式来切换显隐。具体来说就是不要在路由页面A里写iframe也不要离开页面时销毁它而是在layout层比如主框架的content区统一维护一组iframe容器。如果只嵌入一个第三方系统的多个页面就只维护一个iframe修改它的src即可如果嵌入的是多个互相独立的第三方系统或者需要在多个Tab之间保留各自状态就维护多个iframe通过v-show控制当前显示哪一个。用v-show控制显隐本质上和浏览器多标签页的行为一模一样标签页切换时其他标签页的页面并没有被销毁只是从屏幕上移开了。这样iframe内部那个“真实网页进程”就能保留下来。以代码来理解v-show的底层是display属性切换display:none不会把元素从DOM树中移除所以iframe不会被浏览器重新加载。2.2 为什么v-if做不了这件事很多人一开始会用v-if来控制iframe的显示因为v-if写起来顺手条件为true就渲染一个iframefalse就不渲染。但v-if对应的是“创建/销毁”一旦条件变成falseiframe节点会从DOM中彻底移除再次变为true时会重新创建一个全新的iframe并设置src属性结果就是完整地重新加载一遍。我见过不少项目里是这样写的iframe v-ifactiveTab map :srcmapUrl / iframe v-ifactiveTab report :srcreportUrl /这种写法在切换Tab的一瞬间前一个iframe被销毁后一个重新创建。如果两个地址是同一域内的系统还能看到明显的加载过程跨域系统则会出现白屏、loading、重新渲染甚至一些系统还会因为session失效跳回登录页。用户体验极差。换成v-show之后所有iframe从进入页面时就一次性渲染出来切换只是改display不再有任何重新加载过程。这里必须提一个使用v-show时的关键细节多个iframe同时存在时后加载的iframe可能会抢占焦点某些第三方页面在初始化时会尝试让自己“浮到最上层”。这种问题需要通过iframe的隐藏策略或父容器隔离来规避具体会在后面坑位部分展开。3. 多页签场景的完整实现3.1 页面结构设计用Tabs驱动iframe真正能落地的写法通常是配合一个Tabs标签页来做。业务侧逻辑很简单菜单栏打开一个页面时往tabList中追加一条记录再次打开相同页面时只是把activeId切到那条记录上关闭标签页时再从tabList中移除对应记录。所有iframe与tabList一一对应通过activeId控制显隐。每个tab对象建议维护下面几个字段字段用途说明id唯一标识最好用路由name或者页面URL避免重复title标签页标题同时作为iframe的title与展示文案url第三方页面真实地址仅在首次激活时写入srcloaded是否已经加载真实src用于懒加载优化避免一次性加载全部页面scrollTop记录iframe内部滚动位置同源iframe可用跨域时只能记录外层容器位置这套结构可以直接支撑后台管理里的“多页签”功能。相比把iframe散落在各个路由组件里集中管理的好处是状态可见、生命周期清晰、问题更容易排查。这个设计在前期看似多写了一些代码后面换系统、加页面时会省大力气。3.2 组件代码组合式API写法下面给出一份可运行的Vue3代码然后拆解关键逻辑。template div classiframe-page iframe v-fortab in tabList v-showtab.id activeId :keytab.id :data-tab-idtab.id classiframe-box :srctab.loaded ? tab.url : about:blank loadhandleFrameLoad(tab) / /div /template script setup import { ref, watch } from vue const tabList ref([]) const activeId ref() function openTab(tabInfo) { const existed tabList.value.find(t t.id tabInfo.id) if (existed) { activeId.value existed.id } else { const newTab { id: tabInfo.id, title: tabInfo.title, url: tabInfo.url, loaded: false, scrollTop: 0 } tabList.value.push(newTab) activeId.value newTab.id newTab.loaded true } } function handleFrameLoad(tab) { setTimeout(() { if (tab.id activeId.value) { restoreScroll(tab) } }, 50) } function restoreScroll(tab) { const iframe document.querySelector(.iframe-box[data-tab-id${tab.id}]) if (!iframe || !iframe.contentDocument) return iframe.contentDocument.documentElement.scrollTop tab.scrollTop || 0 } watch(activeId, (newId, oldId) { const oldTab tabList.value.find(t t.id oldId) if (oldTab) { const iframe document.querySelector(.iframe-box[data-tab-id${oldId}]) if (iframe iframe.contentDocument) { oldTab.scrollTop iframe.contentDocument.documentElement.scrollTop } } }) /script注意几个细节给iframe挂的是>const refreshToken ref(0) function refreshFrame() { refreshToken.value }第二种直接操作contentWindow。如果是同源页面可以调用iframe.contentWindow.location.reload()跨域情况下无法调用该方法只能采取key重建或修改src加随机参数。第三种在src后面拼接一个随机query或时间戳。这种办法对大多数页面有效但某些跨域站点会对额外query敏感甚至可能因为参数变化而触发权限校验失败所以需要看第三方系统是否支持。我实际项目中更常用的是key重建因为无论同源跨域它都有效而且语义清晰。要注意的是key改变会重新加载因此该操作应强制放到业务按钮的确认交互中避免用户误触导致工作丢失。5. 实操中容易踩的坑与处理清单5.1 iframe通信postMessage与组件生命周期解耦iframe与主应用通信大多使用window.postMessage。如果主应用在某个组件里挂载listener而该组件随着路由销毁了那么消息就接收不到了。多iframe v-show方案已经保证了iframe常驻但通信监听如果跟着页面组件走还是会出问题。我建议把通信监听挂在layout层或者全局唯一的模块上收到message后再根据data中的sourceId或者type字段分发到具体的iframe容器或业务store。不要在每个页面组件内部随意addEventListener更不要在iframe被隐藏时依赖某些可见性生命周期去重连。通信层和展示层解耦之后iframe切来切去消息通道始终保持稳定。这里给一个简单的通信分发思路主应用在layout的mounted中注册window.addEventListener(message, handler)handler里根据event.data.frameId找到对应的tab再调用该tab对应的业务方法。因为layout本身常驻iframe也常驻消息链路是完整的。5.2 滚动位置、尺寸塌陷与隐藏容器display:none虽然保住了iframe的进程却会让其尺寸塌陷部分iframe内部的懒加载图片或图表在隐藏状态下无法正确初始化。处理思路是首次激活时等iframe的load事件触发后再统一执行一次内部异步刷新或者resize。跨域时无法访问contentDocument可以在iframe外层的wrapper上使用min-height/max-height给足空间然后再让内容自适应。滚动位置的保留也需要区分同源和跨域同源可以访问contentDocument改scrollTop跨域无法读取能做的只有记录外层容器scrollTop或者用事件冒泡把内部滚动信息通过postMessage上报给父页面。记得在tab切换离开前保存在load回调恢复。上面代码里的watch activeId正是为此设计的离开时记录老tab滚动位置新tab加载完成后尝试恢复。5.3 多iframe的内存与加载数量控制多iframe方案最大的代价是内存占用。每个iframe都会创建一套完整的浏览器渲染上下文包括HTML文档、JS引擎实例、样式系统等。在一个中后台里无限制地保存所有访问过的Tab内存会越来越大时间一长页面就越来越卡。所以要让iframe数量有上限。常用的做法给Tab列表加一个最大长度比如8个当新Tab超出上限时关闭最久未激活的那个Tab。更优雅一点是LRU每次激活Tab时更新最近使用时间超量时淘汰最久未使用的Tab。被淘汰的Tab不仅要移出tabList还需要把它对应的iframe销毁以释放内存。这里给一个伪LRU的组织思路const MAX_TABS 8 function activateTab(tabInfo) { const existing tabList.value.find(t t.id tabInfo.id) if (existing) { existing.lastUsed Date.now() activeId.value existing.id return } if (tabList.value.length MAX_TABS) { const lru tabList.value.reduce((a, b) (a.lastUsed b.lastUsed ? a : b)) tabList.value tabList.value.filter(t t.id ! lru.id) } tabList.value.push({ ...tabInfo, loaded: true, scrollTop: 0, lastUsed: Date.now() }) activeId.value tabInfo.id }这个LRU策略是“淘汰标签页”而不是“缓存状态”淘汰后的页面下次打开会重新加载。但好在用户通常只会在几个常用页面之间来回切换8个上限足够覆盖日常使用。5.4 常见问题速查表现象根因解决方案切走再切回iframe重新加载iframe节点被销毁或src被重设改用v-show常驻确保节点不离开文档树keep-alive包了组件还是刷新keep-alive缓存的是Vue状态不是iframe原生上下文提升iframe到layout层用v-show控制显隐多个iframe一次性全部加载src直接绑定导致display:none也加载用about:blank占位首次激活才赋值真实urliframe内部滚动位置丢失切换时未保存scrollTop同源时操作contentDocument跨域时用postMessage回传postMessage消息偶尔收不到监听器挂在了会被销毁的组件上把监听器提升到layout层按frameId分发页面越来越卡保留的iframe数量过多用LRU或最大tab数量限制淘汰最久未用页面上面这张表基本覆盖了我在多个项目里遇到的高频问题。每次排查iframe相关问题时先确认“这个iframe节点有没有真的离开文档树”再确认“src有没有被重新赋值”这两个排查点能解决80%的疑难杂症。6. 这套方案在实际项目里的使用体会6.1 项目落地时最推荐的三步走做中后台项目落地多页签iframe无刷新方案整体落地顺序可以这么走先在layout层建立常驻iframe容器给所有页面统一走Tabs机制再用v-show切换然后把iframe的初始src全部设置为about:blank首次激活才赋真实地址最后用LRU控制iframe数量配合全局postMessage做通信分发。这套方案实施后的好处很明显用户来回切换不同后台系统不再是重新加载操作状态完全保留体验提升非常直接。踩过几次坑之后我自己的体会是iframe缓存的本质不是“缓存状态”而是“保持节点存活”。只要理解了这一点很多稀奇古怪的坑其实都能想明白为什么keep-alive不生效为什么v-if刷新为什么display:none不影响内部进程。6.2 什么时候不需要用这套方案也不是所有嵌入场景都需要这么复杂。如果iframe里只是一个静态页面或者被嵌入页面本身不需要保留任何状态那完全没有必要常驻iframe。一个简单的v-if或动态src就够了。另外如果主产品和第三方系统是同域环境可以优先考虑用页面级状态管理或路由信息传递来替代iframe避免iframe带来的通信成本和样式控制成本。只有那些不能改内部代码、必须iframe的第三方页面才值得用这套无刷新缓存方案。我现在的习惯是在做技术方案设计阶段就把“是否需要缓存iframe”作为独立问题拿出来讨论把所有需要常驻的业务页面先列出来再决定用哪种iframe策略。早一点想清楚后面就不会在keep-alive的坑里浪费太多时间。
阅读完成 · 觉得有帮助?
咨询建站