qiankun微应用多页签缓存方案

1,104 阅读2分钟

基于qiankun实现多页签打开微应用缓存方案,通过vue keep-alive实现页面缓存。

实现思路

整体 实现思路 比较简单

1、在微应用的 unmount 的时候 把当前 Vue 实例对象 存到 一个自定义 appCacheMap 对象中, 供下次进入使用;

2、微应用 mount时,将 步骤1 存的实例对象 重新挂载到当前新实例上。

需要注意的是 在mount时, 必须在new Router路由实例,不然路由切换会没有响应。

路由 router-view 需要包上 keep-alive

qiankun 加载注册子应用 registerMicroApps loadMicroApp 等相关内容,可以点击 官网 查阅。

关于卸载的问题

当同个微应用 其中一个标签 关闭了,再次打开 还是会缓存。

一个微应用 下 已经有打开的页面, 在打开一个新页面(之前已经打开过,关闭了的,缓存了vnode), 打开的新页面 还是命中缓存。

解决思路:

关闭标签的时候 保存当前缓存的标签信息 closeCachedViews,微应用监听路由 根据‘当前的打开标签信息’ 和 ‘关闭保存的标签信息‘ closeCachedViews 判断是否 重新打开的已关闭页面,设置 keep-aliveinclude 移除 不缓存 该页面, 然后 移除 ‘关闭保存的标签信息’closeCachedViews 当前值。

代码内容

1、unmount内容

export async function unmount() {
  const needCached = instance.cachedInstance || instance;
  const cachedInstance = {};
  const _router = instance.$router
  cachedInstance._vnode = needCached._vnode;
  // keepalive设置为必须 防止进入时再次created,同keep-alive实现
  if (!cachedInstance._vnode.data.keepAlive)
      cachedInstance._vnode.data.keepAlive = true;
  let routeFlatArr = appCacheMap[key]?.routeFlatArr || _router.options.routes.flatMap(item => { return item.children ? [item, item.children] : [item] }).flat()

  
  appCacheMap[key] = {
      ...cachedInstance,
      // $router: _router,
      apps: [].concat(_router.apps),
    	routeFlatArr
      // currentRoute: { ..._router.currentRoute },
  }
  // 卸载实例
  // instance.$destroy();
  // instance.$el.innerHTML = '';
  // 设置为null后可进行垃圾回收
  // instance = null;
}






2、mount内容

export async function mount(props) {
  // 必须在这里new Router实例,不然路由切换会没有响应
  resetRouter()

  const appCache = appCacheMap[key] || ''
  let vnode = appCache._vnode || null;
  if (appCache) {
      // appCacheMap实例存在, 使用缓存
      // 让当前路由在最初的Vue实例上可用
      router.apps.push(...appCache.apps);

    	// 当前路由path。 router.currentRoute.path 获取不到,只能通过这种方式
      let currentRoutePath = location.hash.match(/#\/(\S*)/)[1].replace(/\?.*$/, "")

      // 通过path 查找 路由信息
      let currentRoute = appCache.routeFlatArr.find(item => item.path === currentRoutePath)
      // 保存关闭事的 缓存路由数组
      console.log(window.microApp.closeCachedViews)
      // 判断 当前打开的 路由path 是否包含在 已关闭的数组中
      if (window.microApp?.closeCachedViews && window.microApp.closeCachedViews.includes(currentRoute.name)) {
          // 中转刷新页面
          proxyRouter.replace({
              path: "/redirect" + location.pathname + "#/refresh?currentName=" + currentRoute.name + "&currentPath=" + currentRoute.path + location.search.replace('?', '&')
          });
      }
  }
  instance = newVueInstance().$mount(
    container
      ? container.querySelector("#micro-app-child")
      : "#micro-app-child"
  );
}

// 实例化子应用vue-router
resetRouter(){
  router = new Router({
    // mode: 'history', // require service support
    base: "/app1",
    scrollBehavior: () => ({ y: 0 }),
    routes: [...],
  });
}

// 创建子应用实例
newVueInstance(cachedNode) {
    const config = {
        router: router, // 在vue init过程中,会重新调用vue-router的init方法,重新启动对popstate事件监听
        store,
        render: cachedNode ? () => cachedNode : (h) => h(App), // 优先使用缓存vnode
    };
    return new Vue(config);
}




3、微应用 router-view 路由拦截内容

<template>
  <el-main class="scroll-bar">
    <keep-alive :include="cachedViews">
      <router-view class="page-container" :key="key" />
    </keep-alive>
  </el-main>
</template>

<script>
export default {
  name: "AppMain",
  data() {
    return {
      cachedViews: [],
    };
  },
  mounted() {
    this.setViews();
  },
  watch: {
    $route: {
      handler() {
        this.setViews();
      },
      deep: true,
    },
  },
  computed: {
    key() {
      return this.$route.path;
    },
  },
  methods: {
    setViews() {
      let tmp = window.microApp.cachedViews;
      if (
        window.microApp?.closeCachedViews &&
        window.microApp.closeCachedViews.length &&
        window.microApp.closeCachedViews.includes(this.$route.name)
      ) {
        // 匹配到当前打开路由 在 closeCachedViews 关闭内, 不缓存
        tmp.splice(tmp.indexOf(this.$route.name), 1);
        window.microApp.closeCachedViews.splice(
          window.microApp.closeCachedViews.indexOf(this.$route.name),
          1
        );
      }
      this.cachedViews = tmp;
    },
  },
};
</script>

4、中转刷新页

<template>
  <div class="app-main" v-loading="true"></div>
</template>
<script>
export default {
  data() {
    return {};
  },
  created() {
    this.refresh();
  },
  methods: {
    refresh() {
      let { query } = this.$route,
        { currentPath, ...otherQuery } = query;
      this.$router.replace({ path: "/" + currentPath, otherQuery });
    },
  },
};
</script>