微前端治理前端应用(三)- 子应用多标签页缓存

584 阅读3分钟

前言

在前期的文章微前端治理前端应用(一)- 聚合新旧应用已经提到过,笔者在项目利用微前端的思路实现了技术栈各种的新旧应用聚合到一个平台,实现新旧业务重新整合,一站式解决业务的所有问题。接下来,产品同学提出了体验优化的需求,聚合的平台菜单繁多,希望界面提供标签,浏览过的历史页面能够记忆页面状态,可操作是否清除缓存。

Github: github.com/simpleKeepe…

技术方案

1、需求梳理

1719975545318.png 业务需求如上图所示,1)菜单切换,基座应用调用不同的子应用路由,菜单切换后回来原界面状态保留,无需再次查询;2)菜单对应的页面是否需要缓存可有用户自由控制

2、疑难点

1)qiankun机制,应用间的切换会有应用卸载和加载的过程,应用会初始化,如果需要子应用切换历史页面状态要保留,就需要将缓存的状态作为共享数据存储,在菜单切回时去回显;2)在聚合平台这种大量菜单的工程里面须合理方式存储,能历史页面回显改造成本较小,否则将造成较大的工作量。

3、解决方案 1)子应用借助Vue官方提供的keep-alive 组件保存子应用实例(详见vue官方文档),keep-alive 是通过组件名称判断缓存的, 其中includes存放需要缓存的组件名称,代码如下:

<template>
  <div class="container">
    <transition name="fade-transform" mode="out-in">
      <keep-alive :include="FOR_MAIN ? KEEPALIVE_FOR_MAIN : aliveList">
        <router-view :key="$route.fullPath"></router-view>
      </keep-alive>
    </transition>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  name: 'App',
  data() {
    return {
      aliveList: ['menuVue21', 'menuVue22', 'menuVue23', 'menuVue24']
    }
  },
  computed: {
    ...mapState(['KEEPALIVE_FOR_MAIN', 'FOR_MAIN'])
  }
}
</script>

2)深入理解Vue模板原理,vue文件编辑解析最后执行生成的都是Vnode,借助这个特性,可联想到要缓存页面状态,可缓存该页面的Vnode。子应用卸载时,缓存子应用vue实例的vnode,子应用再次挂载时,不解析App.vue组件,而createApp的render函数返回vnode,再次渲染子应用的dom时,之前的操作过的界面状态回显,示例代码如下:

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  const cachedInstance = instance.cachedInstance || instance
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance
  const cachedNode = cachedInstance._vnode
  // 让keep-alive可用
  cachedNode.data.keepAlive = true
  cachedInstance.$el = null
  // 卸载当前实例,缓存的实例由于keep-alive生效,将不会真正被销毁,从而触发activated与deactivated
  // instance.$destroy()
  console.log('卸载vue2的示例', instance)
  router = null
  instance = null
}

3)当基座应用再次切换路由回到之前查阅过的历史页面时,如果这时容器涉及到子应用的切换,则当前的子应用会触发mount,此时要注意的是,new Vue()时,render函数中h的入参不再是初始化是的App.vue,而是缓存的cachedNode,示例代码如下:

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  Vue.prototype.$globalState = props
  store.commit('SET_FOR_MAIN', props.FOR_MAIN)
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log('vue2应用接收到的state: 变更后的状态和变更前的状态', state, prev)
    if (state.witchChange === 'clearCache') {
      store.commit('SET_CLEARCACHE', true)
    } else if (state.witchChange === 'changeCacheMenu') {
      store.commit('SET_KEEPALIVE_FOR_MAIN', state.vue2_cacheMenu)
    }
  })
  init(props)
}
let init = (props) => {
  const { container } = props
  // 这里必须要new一个新的路由实例,否则无法响应URL的变化。
  router = new VueRouter({
    base: '/',
    routes,
    scrollBehavior(to, from, savedPosition) {
      if (savedPosition) {
        return savedPosition
      }
      return {
        x: 0,
        y: 0
      }
    }
  })
  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__
    const cachedNode = cachedInstance._vnode
    console.log(cachedInstance, cachedNode)
    router.apps.push(...cachedInstance.$router.apps)
    instance = new Vue({
      router,
      store,
      render: () => cachedNode
    })
    console.log('注入新的router后', instance)
    // 缓存最初的Vue实例
    instance.cachedInstance = cachedInstance
    instance.$mount(container ? container.querySelector('#app') : '#app')
  } else {
    instance = new Vue({
      router,
      store,
      // eslint-disable-next-line
      render: (h) => h(App)
    }).$mount(container ? container.querySelector('#app') : '#app')
  }
}

4)菜单对应的页面是否需要缓存可有用户自由控制,这个借助于keep-alive 组件的include属性,通过添加或者提出对应组件的name,则可实现自定义缓存。

总结

这种方案为实际项目中已落地方案,借助于Vue的虚拟dom这个特性,在子应用切换时,缓存指定页面的Vnode,再次菜单切换时,不再编译、解析App.vue文件,生成新的Vnode,而是直接render执行之前缓存的Vnode,达到历史页面恢复卸载前效果,开发工作量极低。

如果觉得这篇文章帮助到你,请给个赞哦,将给笔者更多写作的信心~~~