likes
comments
collection
share

qiankun微前端项目手动挂载多实例场景项目实践(vue2)

作者站长头像
站长
· 阅读数 69

项目背景

公司平台项目是基于qiankun的微前端应用。主、微应用皆是vue2+webpack4。采用基于路由配置挂载子应用的方式。这种方式的优点在于自动激活微应用。当路由url变化时,会和activeRule规则进行匹配,若匹配成功则当前应用会被激活,

产品需求

用户反馈:菜单栏太多,我需要同时看A微应用下的a菜单和B微应用下的b菜单,反复切来切去非常痛苦。

需求内容:在已有导航栏菜单的基础上,再加一个标签导航栏,要支持手动移除

qiankun微前端项目手动挂载多实例场景项目实践(vue2)

需求方案:这个需求本身比较简单,store中定义一个数组,在router.beforeEach中新增元素,在点击移除按钮时删除元素。这里有一个特殊的应用场景,list跳转到detail、add、update等编辑状态的路由时,需要更新标签的路由path属性值,下次点击标签时,依然停留在上次编辑画面,所以没有在点击导航栏菜单的时候新增数组元素。

需求二次迭代

用户反馈:当新增一个产品时,由于创建流程比较长,在创建的过程中,还要去其他页面查看相关数据,再切回来之后,发现刚刚录入的数据都丢了。

需求内容:用户切换标签后,需要保持之前编辑的数据

需求方案:子应用需要做缓存。vue提供了keep-alive,include prop定义为正则表达式,在需要缓存的组件里定义name属性,其值能匹配上该正则即可。

需求三次迭代

用户反馈:大部分场景都没问题,但是a菜单和b菜单来回切换时,数据要丢失。

问题分析:业务大部分的场景是在同一个微应用下的菜单来回切换,而这里的a菜单和b菜单分别在2个不同的微应用下面。目前采用的是activeRule自动匹配路由激活微应用的方式,它最大的问题是单实例场景。匹配上B微应用后,会先卸载A微应用,再激活B微应用,下次再切到a菜单时,A微应用重新挂载,缓存肯定已失效。

技术本质是qiankun挂载多实例场景。

qiankun多实例实践方案

首先,查阅qiankun官网,微应用配置参数与路由匹配方式是一模一样的。通过手动调用loadMicroApp(app, configuration?)的方式挂载微应用。这个方法返回MicroApp微应用实例,通过调用MicroApp.unmount()手动卸载微应用。

实践开始后,(开发初期,也去各个社区搜索了很多文章,要么内容不符,要么内容太浅,要么没给出具体可落地的方案,但也收获了一些有价值的内容)发现第一个问题,原来是单实例场景,一个dom容器来接收微应用。现在要实现多实例场景,就需要多个dom容器来接收。

step1: 子应用配置放在一个microApps.json文件中,好处在于方便维护,一个微应用有三个重要的属性,name - 名称,port - 端口号(本地开发时的端口号),activeRule - 激活规则。

// microApps.json

[
    {
        "name": "microapp1",
        "port": "3001",
        "activeRule": "/microapp1/"
    }, {
        "name": "microapp2",
        "port": "3002",
        "activeRule": "/microapp2/"
    }
]

step2: 多个dom容器接收对应的微应用。这里也体现出配置文件的意义。

优势在于简单粗暴开发效率高。不足之处在于如果同时激活的微实例数量太多,缓存组件也多,缓存组件还特别大,浏览器资源占用过大,导致页面卡顿。目前项目上3个实例同时运行,同时缓存几个体积大点的页面的情况下,页面操作依然很流畅,所以暂不考虑这个问题。

// Layout.vue

import microAppsCfg from '@/config/microApps'

<template>
    <div
      v-for="app in microAppsCfg"
      v-show="$route.path.startsWith(app.activeRule)"
      class="subapp-container"
      :id="`app_${app.name}--container`"
      :key="app.name"
  />
</template>

step3: 手动挂载微应用。

把方法封装在一个loadMicroApp.js中,方便维护。

// loadMicroApp.js

import microAppsCfg from '../config/microApps'
import { loadMicroApp, addGlobalUncaughtErrorHandler } from 'qiankun'
// 其他依赖。。。

const apps = microAppsCfg.map(app => ({
    ...app,
    entry: getEntry(app), // 把port转成微应用地址
    props: { baseUrl: app.activeRule, ...initialState }, // 传递给微应用的数据
    container: `.app_${app.name}--container`
}))

// 已激活的实例
const activeApps = {}
// 挂载app的方法
const mountMicroApp = path => {
  const app = apps.find(item => path.startsWith(item.activeRule))
  if (app) {
    const instance = activeApps[app.activeRule]
    if (instance) {
      instance.update()
    } else {
      activeApps[app.activeRule] = loadMicroApp(app) // 手动加载子应用
    }
  }
}
// 卸载app的方法
const unmountMicroApps = multipleTabsList => {
  for (const key in activeApps) {
    const isExist = multipleTabsList.some(
      tab => tab.url && tab.url.startsWith(key)
    )
    if (!isExist) {
      activeApps[key].unmount() // 手动卸载子应用
      delete activeApps[key]
    }
  }
}

export { mountMicroApp, unmountMicroApps }

这里维护了activeApps - 已激活的实例集合,mountMicroApp - 挂载app的方法,unmountMicroApps - 卸载app的方法。

step4: 踩坑点-切换菜单后,子应用不更新。

根据官网描述,微应用需要多导出一个update钩子。多次尝试后,这样写就没问题

export async function mount(props) {
  render(props)
}

// 新增:导出一个空的 update 钩子即可
export async function update() {}

export async function unmount() {
  vueInst.$destroy()
  vueInst.$el.innerHTML = ''
  vueInst = null
  router = null
}

再回到mountMicroApp方法里,第六行关键代码 instance.update(),子应用路由终于正常更新了。

step5: 挂载微应用的时机。

放在路由跳转前钩子里比较合适。

// router/index.js
router.forEach((to, form, next) => {
    mountMicroApp(to.path)
})

step6: 卸载微应用的时机。

当维护的标签数组发生变化时,调用卸载微应用的方法比较合适。acitons.js维护的是qiankun通信的action实例。这是一个典型的发布订阅模式。通过initGlobalState生成一个action实例,当updateMultipleTabsList标签数组发生变化时,执行订阅的回调函数,此时调用unmountMicroApps卸载方法。

// actions.js

import { initGlobalState } from 'qiankun'
import { unmountMicroApps } from '@/core/loadMicroApp'

export const initialState = {
  themeColor: '#003993', // 主题色
  baseWindow: window,
  updateMultipleTabsList: [] // 多页签更新列表,子应用监听处理删除keep-alive缓存
}
const actions = initGlobalState(initialState)

actions.onGlobalStateChange(state => {
  unmountMicroApps(state.updateMultipleTabsList)
})

export default actions

这里在主应用的store里维护了updateMultipleTabsList数组,同时在通信的GlobalState里也维护着同一个updateMultipleTabsList数组。

step6: 微应用keep-alive缓存的生命周期。

首先,当标签栏发生变化时(新增、删除、修改),利用qiankun提供的通信方式通知各个子应用。

actions.setGlobalState({
  updateMultipleTabsList: multipleTabsList
})

最后剩下如何手动清除微应用keep-alive中缓存的菜单组件。网上资料也挺多的。这里我的视线思路大概如下: router监听判断是否是缓存菜单:

curNode.$vnode.data.keepAlive

获取keep-alive缓存列表数据:

// {[cid]: componentInstance}
const cache = curNode.parent.componentInstance.cache
// [cid]
const keys = curNode.parent.componentInstance.keys

获取当前组件的cid:

const cid = curNode.componentOptions.Ctor.cid

删除组件缓存:

delete cache[cid]
keys.splice(keys.indexOf(cid), 1)
curNode.$destroy()

结语

以上就是qiankun微前端架构下的页签缓存功能的踩坑史,希望对大家有所帮助。

转载自:https://juejin.cn/post/7210639699584761915
评论
请登录