qiankun微前端项目手动挂载多实例场景项目实践(vue2)
项目背景
公司平台项目是基于qiankun的微前端应用。主、微应用皆是vue2+webpack4。采用基于路由配置
挂载子应用的方式。这种方式的优点在于自动激活微应用。当路由url变化时,会和activeRule
规则进行匹配,若匹配成功则当前应用会被激活,
产品需求
用户反馈:菜单栏太多,我需要同时看A微应用下的a菜单和B微应用下的b菜单,反复切来切去非常痛苦。
需求内容:在已有导航栏菜单的基础上,再加一个标签导航栏,要支持手动移除。
需求方案:这个需求本身比较简单,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