稀土掘金技术社区 06月03日 09:47
为了让 iframe 支持 keepAlive,我连夜写了个 kframe
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文针对基于Vue3全家桶开发的管理后台系统中,多标签页模式下Iframe内容区在切换标签后自动刷新的问题,提供了解决方案。由于KeepAlive缓存机制与Iframe的独立浏览上下文特性冲突,导致Iframe无法被缓存,从而引发了刷新问题。作者通过物理隔离与视觉映射的策略,将Iframe的真实DOM节点与Vue组件实例解耦,实现了keepAlive的效果。该方案涉及Iframe类、IFrameManager类和Iframe组件的实现,并提供了详细的代码示例和类关系图。

🧐问题分析:在Vue3多标签页管理系统中,KeepAlive缓存的是Vue组件实例,而Iframe的浏览上下文是独立的,无法被缓存。每次组件activated时,Iframe会重新加载,导致内容刷新和进度丢失。

💡解决方案:将Iframe从Vue组件中分离,使其不依赖于Vue的缓存机制。通过在body节点下创建Iframe,并使用样式控制其显示与隐藏,从而实现Iframe的持久化,避免重新加载。

🛠️具体实现:通过Iframe类封装Iframe操作,IFrameManager类统一管理Iframe,以及Iframe组件的实现。Iframe组件负责创建、显示、隐藏、调整Iframe大小等操作,并结合Vue的生命周期钩子函数,实现Iframe的缓存和复用。

原创 Canmick 2025-06-02 09:00 重庆

点击关注公众号,“技术干货” 及时达!

(创作者训练营强势上线,速戳上图了解)

前几天收到一个bug,说是后台管理系统每次切换标签栏后,xxx内容区自动刷新,操作进度也丢失了,给用户造成很大困扰。作为结丹期修士的我自然不容允许此等存在,开干!

问题分析

该后台管理系统基于 vue3 全家桶开发,多标签页模式,标签页默认 

KeepAlive

,本文以 demo 示例。
切换标签后内容区自动刷新,操作进度丢失?首先想到的是 

KeepAlive

 问题,但经过排查后才发现,

KeepAlive

 是正常的,异常的是内嵌于页面的 iframe 内容区,页面每次 

onActivated

 时,iframe 内容区都会重新加载一次,导致进度丢失。

iframe 并没有被 keep 住,为什么?

通过查阅 Vue 文档得知,

KeepAlive

缓存的只是 Vue 组件实例,组件实例包含组件状态和 VNode (虚拟 DOM 节点)等。当组件 activated 时,组件 VNode 已经转为真实 DOM 节点插入文档中了,而组件 deactivated 时,已经从文档中移除了组件对应的真实 DOM 节点并缓存组件实例。

VNode 是对真实 DOM 节点的映射,包含节点标签名、节点属性等信息。我们打开控制台选中 iframe 元素,右侧那栏就是其对应的 VNode 了。

从上图可看出,iframe 的内容并不属于节点信息,是个独立的 browsing context(浏览上下文),无法被缓存;iframe 每次渲染(如 DOM 节点插入、移动)都会触发完整的加载过程(相当于打开新窗口)。故组件每次 activated 时,iframe 都会重新加载,创建了新的上下文,之前的操作进度自然是丢失了。

至此,问题原因已找到,接下来看下如何处理。

解决方案

iframe 无法保存于 VNode 中,又不能将 iframe 从文档中移动或移除,那么就想办法在某个地方把 iframe 存起来,比如 body 节点下,然后通过样式控制 iframe 展示与隐藏,顺着思路捋一下整体流程。

有了上述流程,开始设计下细节。 Iframe 组件是对 iframe 操作流的封装,方便在 vue 项目中使用,内部涉及 iframe 创建、插入、设置样式、移除等操作,为方便操作,将其封装为 Iframe 类;分散的 Iframe 类操作,稍有不当可能造成内存占用过多,故为了统一管理,再设计一个 IframeManage 来统一管理 Iframe。相关的类关系图如下

对应的时序图如下

至此思路清晰,开始进入编码

编码实战

首先是 Iframe 类的实现

interface IframeOptions {  uid: string  src: string  name?: string  width?: string  height?: string  className?: string  style?: string  allow?: string  onLoad?: (e: Event) =>void  onError?: (e: string | Event) =>void}type IframeRect = Pick<DOMRect, 'left' | 'top' | 'width' | 'height'> & { zIndex?: number | string }class Iframe {  instance: HTMLIFrameElement | null = nullconstructor(private ops: IframeOptions) {    this.init()  }  init() {    const {      src,      name = `Iframe-${Date.now()}`,      className = '',      style = '',      allow,      onLoad = () => {},      onError = () => {},    } = this.ops    this.instance = document.createElement('iframe')    this.instance.name = name    this.instance.className = className    this.instance.style.cssText = style    this.instance.onload = onLoad    this.instance.onerror = onError    if (allow) this.instance.allow = allow    this.hide()    this.instance.src = src    document.body.appendChild(this.instance)  }  setElementStyle(style: Record<stringstring>) {    if (this.instance) {      Object.entries(style).forEach(([key, value]) => {        this.instance!.style.setProperty(key, value)      })    }  }  hide() {    this.setElementStyle({      display: 'none',      position: 'absolute',      left: '0px',      top: '0px',      width: '0px',      height: '0px',    })  }  show(rect: IframeRect) {    this.setElementStyle({      display: 'block',      position: 'absolute',      left: rect.left + 'px',      top: rect.top + 'px',      width: rect.width + 'px',      height: rect.height + 'px',      border: '0',      'z-index'String(rect.zIndex) || 'auto',    })  }  resize(rect: IframeRect) {    this.show(rect)  }  destroy() {    if (this.instance) {      this.instance.onload = null      this.instance.onerror = null      this.instance.remove()      this.instance = null    }  }}

其次是 IFrameManager 类的实现

export class IFrameManager {static frames = new Map()static createFame(ops: IframeOptions, rect: IframeRect) {    const existFrame = this.frames.get(ops.uid)    if (existFrame) {      existFrame.destroy()    }    const frame = new Iframe(ops)    this.frames.set(ops.uid, frame)    frame.show(rect)    return frame  }static showFrame(uid: string, rect: IframeRect) {    const frame = this.frames.get(uid)    frame?.show(rect)  }static hideFrame(uid: string) {    const frame = this.frames.get(uid)    frame?.hide()  }static destroyFrame(uid: string) {    const frame = this.frames.get(uid)    frame?.destroy()    this.frames.delete(uid)  }static resizeFrame(uid: string, rect: IframeRect) {    const frame = this.frames.get(uid)    frame?.resize(rect)  }static getFrame(uid: string) {    returnthis.frames.get(uid)  }}

最后是 Iframe 组件的实现

<template>  <div ref="frameContainer"class="k-frame">    <span v-if="!src"class="k-frame-tips">      <slot name="placeholder">暂无数据</slot>    </span>    <span v-else-if="isLoading"class="k-frame-tips">      <slot name="loading">加载中... </slot>    </span>    <span v-else-if="isError"class="k-frame-tips"> <slot name="error">加载失败 </slot></span>  </div></template><script setup lang="ts">import { onActivated, onBeforeUnmount, onDeactivated, ref, watch } from'vue'import { IFrameManager, getIncreaseId } from'./core'import { useResizeObserver, useThrottleFn } from'@vueuse/core'defineOptions({  name: 'KFrame',})const props = withDefaults(  defineProps<{    src: string    zIndex?: string | number    keepAlive?: boolean  }>(),  {    src: '',    keepAlive: true,  },)const emits = defineEmits(['loaded''error'])const uid = `kFrame-${getIncreaseId()}`const frameContainer = ref()const isLoading = ref(false)const isError = ref(false)let readyFlag = falseconst getFrameContainerRect = () => {const { x, y, width, height } = frameContainer.value?.getBoundingClientRect() || {}return {    left: x || 0,    top: y || 0,    width: width || 0,    height: height || 0,    zIndex: props.zIndex ?? 'auto',  }}const createFrame = () => {  isError.value = false  isLoading.value = true  IFrameManager.createFame(    {      uid,      name: uid,      src: props.src,      onLoad: handleLoaded,      onError: handleError,      allow: 'fullscreen;autoplay',    },    getFrameContainerRect(),  )}const handleLoaded = (e: Event) => {  isLoading.value = false  emits('loaded', e)}const handleError = (e: string | Event) => {  isLoading.value = false  isError.value = true  emits('error', e)}const showFrame = () => {  IFrameManager.showFrame(uid, getFrameContainerRect())}const hideFrame = () => {  IFrameManager.hideFrame(uid)}const resizeFrame = useThrottleFn(() => {  IFrameManager.resizeFrame(uid, getFrameContainerRect())})const destroyFrame = () => {  IFrameManager.destroyFrame(uid)}const getFrame = () => {return IFrameManager.getFrame(uid)}useResizeObserver(frameContainer, () => {  resizeFrame()})onBeforeUnmount(() => {  destroyFrame()  readyFlag = false})onDeactivated(() => {if (props.keepAlive) {    hideFrame()  } else {    destroyFrame()  }})onActivated(() => {if (props.keepAlive) {    showFrame()    return  }if (readyFlag) {    createFrame()  }})watch(() => [frameContainer.value, props.src],(el, src) => {    if (el && src) {      createFrame()      readyFlag = true    } else {      destroyFrame()      readyFlag = false    }  },  {    immediate: true,  },)defineExpose({  getRef: () => getFrame()?.instance,})</script><style lang="scss" scoped>.k-frame {  position: relative;  display: flex;  align-items: center;  justify-content: center;  width: 100%;  height: 100%;  &-tips {    position: absolute;    top: 50%;    left: 50%;    transform: translate(-50%, -50%);  }}</style>

看看效果

小结

管理后台多页签切换,iframe 区操作进度丢失,根本原因在于 

KeepAlive

 缓存机制与iframe 的独立浏览上下文特性存在本质冲突。本文通过「物理隔离」「视觉映射」的双重策略,将 iframe 的真实 DOM 节点与Vue 组件实例解耦,实现了 

keepAlive

 的效果。当然,该方案在代码实现还有很大优化空间,如 IFrameManager 目前是单例模式、Iframe 池未设计淘汰缓存机制(如 LRU )。嘀嘀嘀...产品催着上线了,没时间优化了,下次一定。相关代码已上传 github,欢迎道友们给个 star,在此谢过了

AI编程资讯AI Coding专区指南:

https://aicoding.juejin.cn/aicoding

""~

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Vue3 Iframe KeepAlive 多标签页 前端开发
相关文章