Vue 项目中加入含有 iframe 的页面,同时在路由切换的过程中,要求 已加载过的iframe 的内容不会再被刷新 。Vue自带的 keep- alive 没达到预期效果

目的:为了避免每次的iframe重复加载时间过长,导致体验不好

参考链接
https://www.cnblogs.com/shj-com/p/15049030.html
https://blog.csdn.net/weixin_45677200/article/details/123892101?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-1-123892101-blog-111522544.pc_relevant_3mothn_strategy_recovery&spm=1001.2101.3001.4242.2&utm_relevant_index=4
https://www.nhooo.com/note/qa36vm.html

1、序言
最近工作中,项目上遇到一个这样的需求,去切换tab标签,切回打印预览界面的时候,要求界面不刷新。vue框架中,我们去处理此类问题,通常马上就会想到去使用vue框架中自带的keep-alive组件,所以一开始我也是去使用了keep-alive,但是发现没有达到预期效果,后面通过研究和查阅资料发现,在vue项目中加入了含有iframe的页面,在路由切换的过程中,使用keep-alive是达不到数据缓存的效果的。

2、使用keep-alive缓存不了iframe界面原因
(1)vue中的keep-alive
【1】原理:Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode节点。因此,Vue 的 keep-alive 缓存也是基于 VNode节点 而不是直接存储 DOM 节点。在需要渲染的时候从Vnode渲染到真实DOM上。
【2】参数:Keep-alive 组件提供了 include 和 exclude 两个属性,允许组件有条件的进行缓存。
  include: 字符串或正则表达式。只有匹配的组件会被缓存。
  exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
【3】Keep-alive 组件提供了两个生命钩子函数,分别是 activated 和 deactivated 。
  activated :当页面存在缓存的时候执行该函数。
  deactivated :在页面结束时触发该方法,可清除掉滚动方法等缓存。
(2)iframe中keep-alive机制失效原因:iframe页里的内容并不属于节点的信息,所以使用keep-alive依然会重新渲染iframe内的内容。而且iframe每一次渲染就相当于打开一个新的网页窗口,即使把节点保存下来,在渲染时iframe页还是刷新的。

3、vue中实现iframe内容缓存的思路
(1)保存iframe页里的节点信息我们很难去操作,这个时候我们就该想是否能在vue的route-view节点上动些手脚。
(2)我们可以在切换不含iframe的界面时使用vue路由,在切换含iframe页的界面时利用v-show来控制显示隐藏,使iframe的节点不被删除,以此来防止界面节点被重新更新,从而达到保存iframe节点数据的效果。
具体该怎么操作呢?请继续往下看:

第一步:
修改主界面AppMain.vue(你的可能不叫这个)中的template,添加vue的内置组件component,如下:

<template>
  <div id="app">
    <!--以下加的是vue页面缓存机制-->
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
        </keep-alive>
      <router-view v-if="!$route.meta.keepAlive"></router-view>
    <!-- 以下 vue页面这个是原来的写法(无缓存)-->
    <!-- <router-view /> -->

    <!-- 以下针对于iframe页 -->
    <component
      v-for="item in hasOpenComponentsArr"
      :key="item.name"
      :is="item.name"
      v-show="$route.path === item.path"
    ></component>

  </div>
</template>
<script>
export default {
  data() {
    return {
      componentsArr: [], // 含有iframe的页面
      iframeArr: []
    }
  },
  watch: {
    $route() {
      // 判断当前路由是否iframe页
      this.isOpenIframePage();
    }
  },
  created() {
    var style = localStorage.getItem("style");
    if(style){
      document.getElementById('themeStyle').setAttribute("href",`/public/static/themes/${style}/index.css`); //实现将主题保存在内存中刷新浏览器不改变
    }

    //===========
    // 设置iframe页的数组对象
    const componentsArr = this.getComponentsArr();
    componentsArr.forEach((item) => {
      Vue.component(item.name, item.component);
    });
    console.log("componentsArr(created)",componentsArr);
    this.componentsArr = componentsArr;
    // 判断当前路由是否iframe页
    this.isOpenIframePage();
  },
  computed: {

    // 实现懒加载,只渲染已经打开过(hasOpen:true)的iframe页
    hasOpenComponentsArr() {
      console.log("computed()");
      console.log(this.componentsArr);
      return this.componentsArr.filter(item => item.hasOpen);
    }
  },
  mounted(){
    // 关闭浏览器窗口的时候清空浏览器缓存在localStorage的数据
    window.onbeforeunload = function (e) {
      var storage = window.localStorage;
      //以下为了避免不退出登录就切换账号,所以当页面出现刷新的操作,也要去清掉权限组件中的缓存
      localStorage.removeItem("selectqhQX")
      localStorage.removeItem("selectqhQXqy")
      localStorage.removeItem("selectqhQXdt")
    }
  },
  methods:{ // 根据当前路由设置hasOpen
    isOpenIframePage() {
      console.log("isOpenIframePage");
      const target = this.componentsArr.find(item => {
        return item.path === this.$route.path
      });
      if (target && !target.hasOpen) {
        target.hasOpen = true;
      }
    },
    getIframe(arr) {
      arr.map(item=>{
        if(item.iframeComponent){
          this.iframeArr.push(item);
        }
        if(item.children && item.children.length>0) {
          this.getIframe(item.children)
        }
      })
    },
    // 遍历路由的所有页面,把含有iframeComponent标识的收集起来
    getComponentsArr() {
      const router = this.$router;
      const routes = router.options.routes;
      this.getIframe(routes);

      return this.iframeArr.map((item) => {
        const name = item.name || item.path.replace('/', '');
        return {
          name: name,
          path: item.path,
          hasOpen: false, // 是否打开过,默认false
          component: item.iframeComponent // 组件文件的引用
        };
      });
    }
  }
}
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
</style>

该组件主要做的是根据main.js里的routes生成一个只含有iframe页的数组对象。
watch 上监听 route,判断当前页面 在 iframe 页列表里的话就设置 hasOpen 属 性为 true ,渲染该组件用v−show=“route.path === item.path” 切换iframe页的显示与隐藏

第二步:
修改路由配置文件

import Vue from 'vue'
// 引入vue-router
import Router from 'vue-router'
import axios from 'axios'
import shareUrlArr from './share';
import SiderAndCenter from '@/framework/components/SiderAndCenter'
import Gcjs from '@/views/system/release/gcjs/index';
import {getCookie,setInof} from '@/utils/Utils';

//第三方库需要use一下才能用
Vue.use(Router);

const router = new Router({
  mode: 'history', // 采用history模式URL的路径才跟配置的对应上,不然URL是先加/#再追加配置的地
  routes: [
    {
      path:'/sharevue',
      name: '本地菜单',
      component:()=>import("../shareframework/ShareFramework.vue"),
      redirect:"/sharevue/newztgl",
      children:[
        {
          name: "A页面",
          path: "/sharevue/mt",
          iframeComponent:"/sharevue/mt",//用于标识该页面是否含有iframe页面;这个值和path值是一样的
          component: () => import("../views/newWindow/p12/index.vue"),
          meta: {
            keepAlive: true, //此组件需要被缓存
          }
        },
        {
          name: "B页面",
          path: "/sharevue/mtDetails",
          iframeComponent:"/sharevue/mtDetails",//用于标识该页面是否含有iframe页面;这个值和path值是一样的
          component: () => import("../views/newWindow/p12/indexDetails.vue")
        },
        {
          name: "C页面",
          path: "/sharevue/phonesyNew",
          component: () => import("../views/newWindow/p13/index.vue")
        },
        {
          name: "D页面",
          path: "/sharevue/phoneywlNew",
          component: () => import("../views/newWindow/p13/indexywlfx.vue"),
          meta: {
            keepAlive: true, //此组件需要被缓存
          }
        },
      ]
    },
    ...shareUrlArr
  ]
});

//push
const VueRouterPush = Router.prototype.push
Router.prototype.push = function push (to) {
  return VueRouterPush.call(this, to).catch(err => err)
}

//replace
const VueRouterReplace = Router.prototype.replace
Router.prototype.replace = function replace (to) {
  return VueRouterReplace.call(this, to).catch(err => err)
}

const ReleaseRoute = ['/','/login','/ssologin','/404','/500','/test1'];

// 路由守卫
router.beforeEach((to, from, next) => {

});

export default router;

3.拓展
由于切换标签,含iframe的component组件不会再触发路由钩子函数和生命周期函数,导致界面数据无法做更新操作,同时浏览器刷新时,界面数据会有丢失的问题,所以我们可以考虑去监听sessionStorage的值,以此来更新数据。详情可以看下面这篇文章:vue中如何实现对sessionStorage的监听。

keep-alive是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM;导致在组件mounted钩子中调用的刷新页面内容时,这个钩子没有被调用。

故:使用Vue组件切换过程,执行钩子activated(keep-alive组件激活时调用),而不是挂载钩子mounted。
参考以下
https://www.cnblogs.com/candy-xia/p/11425357.html

//activated和deactivated配合keep-alive标签使用!

activated () {
console.log('实例被激活时使用,用于重复激活一个实例的时候')
}
deactivated () {
console.log('实例没有被激活时')
}
//js部分--vue生命周期

beforeCreate () {
    console.log('在实例初始化之前调用')
  }
  created () {
    console.log('在实例初始化之后调用,经常用于操作数据,发起ajax请求')
  }
  beforeMount () {
    console.log('在挂载开始之前被调用,如果是在服务器端渲染时不被调用;在这个函数里,无法获取元素')
  }
 
  mounted () {
    console.log('在挂载后被调用,也不能在服务器端渲染时被调用;这个函数里,是可以获取元素,并进行操作的')
  }
  beforeUpdate () {
    console.log('视图层数据更新前调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM')
  }
  updated () {
    console.log('视图层数据更新后调用')
  }
  beforeDestroy () {
    console.log('实例销毁之前调用,在被销毁的组件中进行调用;有一些操作,会在实例已经销毁的时候还在运行,这时候为了性能考虑,就在这里结束哪些操作')
  }
  destroyed () {
    console.log('实例销毁后调用。')
  }
Logo

前往低代码交流专区

更多推荐