microApp微前端重构多Tab功能

背景:

由于历史原因,原设计为主应用为history路由,子应用为hash路由,现在我们的多tab是单微应用的tab,写在每个微应用中的,跨应用就没有了。失去了导航tab的实际意义,没有提供一致的导航体验,应该无论在哪个微应用中,看到的都是相同的顶层导航,这有助于保持整体应用的一致性和直观感。


目的:

1. 将多tab抽到基座实现统一导航

2. 由于涉及项目较多,尽量改动基座,不要涉及各个子应用大的改动


设计:

  • 创建AppTab公共组件,确定创建,切换,替换,删除规则
  • tabList中存子应用具体的Hash路径,为解决详情页多开的问题
  • 解决主应用与子应用之间路由同步问题
  1. 利用路由的特性,相同路由不进行跳转        
  2. 首次点击菜单,主应用通过dataChange传递pageName,子应用监听后通过pageName跳转到对应hash路由,子应用通过路由守卫以及microApp的虚拟路由,子改主路由,将hash路由反向传递给主应用路由,主应用收到后存入多tab列表,再次点击可以直接从tab列表中获取到对应具体hash路径,通过虚拟路由直接更改已激活子应用的子路由
  3. 对不同的操作,以及特殊情况做特殊处理,如刷新浏览器操作,无预设菜单,详情页多开等做兼容处理。
  • 对主应用路由进行监听,对AppTab进行操作


具体改造点:

公共组件anjuWindow:

1. 主要就是删除tab标签的dom和处理tab的逻辑,只留页面路径keepAlive的逻辑和功能

2. 添加事件监听,用于页面的刷新

onMounted(()=>{
    //用于双键重新加载
	window.addEventListener('refreshPage', refreshPage);
    //用于实时清除页面缓存
    window.addEventListener('cachePageNamesChange', cachePageNamesChange);
})

onUnmounted(()=>{
	window.removeEventListener('refreshPage', refreshPage);
    window.addEventListener('cachePageNamesChange', cachePageNamesChange);
})

主应用:

1. 新增AppTab组件

2. 点击顶级菜单后的处理逻辑 (同点击右侧菜单处理逻辑相同)

3. 点击右侧菜单后的处理逻辑

const getRouteParams = menu => {
	//详情类的未预设菜单,不会触发这里的事件,这里事件触发时,都只能拿到子应用对应pagename
	//判断是否已加载,加载了直接去改子应用路由,没加载通过父传data去改
	const tabList  = JSON.parse(hive.get('tabList'))
	const tabItem = tabList?.find(v=>v.location.params.pageName === menu.pageName)
	if(tabItem ){
        if(tabItem.location.fullPath === route.fullPath) return
		//为解决同菜单下,不同微应用切换,获取当前正活跃的子应用是否包含,准备加载页面的子应用
		const activeApp = getActiveApps({ excludeHiddenApp: true })
		const preAppName = menu?.reAppName || tabItem.appName || currentApp.value.appName
		if(activeApp?.includes(preAppName)){
			microApp.router.push({name: preAppName, path: tabItem.location.params.hashPath})
		}else{
			//使用fullPath,为了不触发主应用路由改变后,tab替换而导致的再次点击找不到params中hashPath,的问题。
			router.push({ path:tabItem.location.fullPath,params: { pageName: menu.pageName } })
		}
	}else{
		router.push({ name: 'microAppRoot', params: { appName:currentApp.value.appName, pageName:menu.pageName } })
	}
}

4. 加载子应用的组件中data新增字段isIntercept

data: { name: pageName,isIntercept:appName !== realAppName}

子应用:

App.vue

删除(0, eval)('window').location.hash?.length > 2 和count 的判断逻辑,在微前端架构中,由于应用被拆分成多个独立的部分,每个部分都有其自己的作用域和上下文。然而,在某些情况下,可能需要访问全局作用域的window对象,尤其是在进行全局状态管理、事件监听或跨框架通信时。(0, eval)('window')的用法实际上是一种技巧,用于确保获取到的是全局作用域的window对象,而不是某个特定作用域内的window。这种技巧在处理嵌套作用域或跨框架通信时特别有用,因为它可以确保访问到正确的全局对象,而不是被当前执行上下文所限制的局部window对象。,原设计应该是为了解决相同应用重复加载的问题,删掉是因为现在的设计对子应用做了缓存,前后台切换,不删跳转会被return
//监听主应用消息,dataChange改造前
onMounted(() => {
	let routes = router.getRoutes()
	let count = 0
	window.microApp?.addDataListener?.(data => {
		if ((0, eval)('window').location.hash?.length > 2 && !count) {
			return
		}
		// 当基座下发跳转指令时进行跳转 需校验下发路由是否在当前应用注册
		if (data.name && route.name !== data.name && routes.some(r => r.name === data.name)) {
			replace({ name: data.name }).catch(e => console.log(e))
			count++
		}
	}, true)

//改造后
onMounted(() => {
	let routes = router.getRoutes()
    let count = 0
    //从表现看,默认路由的跳转
	//时机:是在子应用mounted完成后,并且子应用接收到跳转消息后,在router.replace之前的前置操作
	//解决:拦截默认路由只能在默认路由跳转完之后再创建一个与主应用互动的路由守卫,这样就能解决这个问题
	window.microApp?.addDataListener?.(data => {
        if(data.isIntercept) return
		if (data.name && route.name !== data.name && routes.some(r => r.name === data.name)) {
    setTimeout(() => {
		if(!count){
			router.afterEach(to => {
			const baseRouter = window.microApp.router.getBaseAppRouter() 
			// 控制主应用跳转
			baseRouter.replace({hash:to.href,params:{hashPath:to.href,title:to.meta.title,pageName:to.name}})
	        // 子应用为vue*2x,vue-router*3x时,由于路由守卫返回值to字段不同,特殊处理
			//baseRouter.replace({hash:'#' + to.fullPath,params:{hashPath:'#' +  to.fullPath,title:to.meta.title,pageName:to.name}})
			})
			}
			replace({ name: data.name })
			count++ 
			}, 100);
		}
	}, true)
router.js
//路由守卫
router.beforeEach(async (to, from, next) => {
    //确保有这个,现在有的子应用之前就有,有的没有
	if (!from.name && '404' === to.name) return false

	...
})
router.config.js
将顶级路由的component改为改造后的AnjuWindow,vue3项目安装特定版本的组件库直接用AnjuWindow即可,vue2项目由于用的element-ui,为避免冲突暂时写个vue2版的AnjuWindow放本地处理

遇到的问题:

1. 前期设计阻碍较大

原因:

  •  前期进行初步了解后,想依据之前的经验,去复用一些解决方案,但是发现区别太大
  • 对当前微前端框架不太熟悉
  • 由于系统菜单存在多个特殊的应用场景,如菜单为kbs,但内部嵌有oa的子应用菜单,菜单通过路由appName渲染,点击时要保持菜单不变,但要渲染对应子应用,以及包含详情多开,或动态tab标签等,设计时需要考虑全面,整个链路较长,多个应用间调试不方便,在路由监听,路由守卫等监听中进行逻辑处理,稍有不慎就会进入死循环。

解决:下载mircoApp框架的demo,熟悉配置项以及常用api,将初步设计方案在demo中实现,排除业务逻辑可能发生的影响,对应基座子应用等项目找出对应问题,利用路由特性实现整个链路的闭环,再修复完善一些特殊情况的兼容以及问题。


2. 由于同步路由的操作,导致路由堆栈混乱,浏览器回退前进存在问题

原因:应该是mircoApp的虚拟路由中子改主,或主改子路由时,会对浏览器的路由栈做一些特殊处理,如清空,浏览器的回退前进操作时,表现是主应用路由和子应用路由都别改变了,加上写了很多监听路由的逻辑,偶现会死循环跳转。

解决:将子应用中的事件监听dataChange的触发中改变路由的改为replace,以及路由守卫中的通过虚拟路由子改主路由的方式也改为replace。


3. 首次加载子应用会在接收到datachange后,子应用replace前,有个前置默认跳转的操作,触发路由守卫(直接操作的路由),导致多tab多弹一个以及报错。

原因:

  • microApp框架设计如此,当主子应用使用路由不同时,一个history一个hash,首次加载子应用时,会先跳默认路由找到子应用,再通过事件监听改变子应用路由
  • 默认路由一般是’/‘,通过表现,发现默认路由在from子应用状态经过后台推前台的操作会改变,也就是默认路由其实是动态的,由于测试时只进行了两个子应用的测试,一直没发现问题,多个子应用一起运行切换时才发现,所有之前解决方案pass。

之前解决方案:不使用‘/’的路由,路由守卫做拦截,等于‘/’不触发改主应用逻辑,这样路由内部会抛出一个未找到对应路由的错误,通过onError在外部接住就行。暂时没有想到更好的方式。

现在解决方案:确定了默认路由跳转的时机,通过在默认路径跳转完成之后,动态挂载一个新的路由守卫,处理与主应用路由同步的逻辑。


4. 在绩效界面(oa)时,点击kbs工作台,删除工作台tab,删除完成后会新增一个oa的工作台tab。

原因:子应用间切换时,时机就是在当前子应用(a应用)afterhidden后,准备加载新子应用(b应用)beforeshow之前,路由改变,microOptions随之改变,会主动向b应用发送a应用最新的消息。b应用就会触发dataChange事件。只有工作台存在这个问题是因为oa,kbs都有工作台(dashboard)这个路由,而我们拦截的方式是通过

if(!router.getRoutes().some(r=>r.name===data.name)) return

例如:绩效打开任务管理,关闭任务管理,触发kbs的afterhidden,然后进行默认的操作,就是会主动向oa应用发送kbs应用最新的消息(也就是{name:taskManage}),oa收到消息后检索routes中不存在这个name的路由,就拦截掉了。反之工作台,走到这一步能找到name为dashboard的路由,就会触发接下来的逻辑,跳转oa工作台,同步主应用路由,新增tab。

解决方案:由于触发这个默认操作的必经的生命周期是afterhidden,afterhidden触发的关键就是即将加载的子应用和当前子应用不一样,所以在data中新增变量isIntercept(是否拦截)

data: { name: pageName,isIntercept:appName !== realAppName}

在子应用中新增判断

if(data.isIntercept) return

5. 404页面特殊情况开两个

原因:由于原设计,path中的appName,其实为表面为两用,一是用于寻找右侧菜单,二是用于渲染对应子应用,但其实主要就只是用于渲染右侧菜单,子应用的渲染第一优先级是其菜单配置中的rePageName, 这个设定在一般场景下不存在问题,但是在特殊场景下,如404,找不到对应菜单配置并且还是在其他子应用右侧菜单中的page(如技管菜单中绩效),就会出现我在oa子应用中的404,加载tab404页面,因为404所以找不到对应的菜单配置,就会使用第二优先级的appName,但appName为kbs,会加载kbs子应用,再加载404的连锁反应。

现解决方案:由于404只会出现在内部点击,不会出现在菜单点击,所以在tab切换处和传入子应用配置这两个地方用到的appName,做一个特殊处理

//通过框架拿到当前正在运行的子应用名称
const realAppName =  getActiveApps({ excludeHiddenApp: true })?.[0]
//如果当前页面为正常页面,currentPage是有值的,那就走原先的逻辑
//如果当前页面是404,那currentPage是undefined,appName就不从菜单配置中取appName,而是就一直保持在激活的子应用,不会触发后续连锁反应
const appName = currentPage.value?.title ? (currentPage.value.reAppName || currentApp.value.appName) : realAppName

其实路由上的appName应该作用唯一,就是加载那个子应用,appName就是那个子应用的名称,规避路径上appName为kbs,实际加载oa这种场景,路径右侧菜单的寻找可以通过其他方式或者定义其他变量,这样这个问题也解决了,我是认为这是最好的解决方法,但是目前用于右侧菜单的变量到底定义在哪里,以及更改时机还没有想到很合适的方案,而且如果这个改掉的话,所有功能逻辑得重新测一遍,不仅耗时可能还会有新的问题


6. a子应用加载A页面完成后,跳转另一个b子应用的B页面,a子应用进入后台缓存,此时点击进入a子应用的C页面,a应用推出前台后会先进A页面闪一下,再到C页面

原因:尝试在子应用中路由守卫处拦截,没用,拦不了后台推前台的那一下闪烁,因为闪烁那一下不是路由跳转导致的闪烁,而是由于子应用有缓存,先加载子应用(之前就在A页面),再跳转路由,感觉是正常现象。

解决方案:暂无。

1. 尝试通过交互loading来掩盖,通过子应用路由守卫监听跳转完成之后再向主应用发送消息改变loading值,这样感觉反向优化了,loading时间增加,维护成本会高,还未实践。

2. 尝试通过动态改变keepalive缓存include顺序,让缓存页面提前激活,发现顺序无法影响页面渲染,具体原因看图,include主要作用是用来对缓存进行处理,主要用于判断有没有缓存,找对应组件缓存是通过组件名称和路由path,源码用到也是indexOf。


7. 部分场景涉及到下一步操作,如活动详情,点击下一步时新增了页签

原因:由于详情多开的设计,页签路由存的是完全精准的路由,活动的下一步也是通过路由控制的,导致按详情多开的场景一样处理了

解决方案:暂无,因为涉及到业务设计,需要与产品商讨,换一种方式来展示下一步,看是改交互还是改实现方式,反正需要与详情多开的场景有所区别

8. 由于此次改版实行方案为逐步替换子应用,存在新老子应用共存的情况,在切换新老子应用时,老的子应用会出现报错导致页面加载不出来的问题

原因:经过排查,此问题由于老应用中存在以下代码,进入了这个判断拦截了子路由的replace。这个问题是由于(0, eval)('window').location.hash的长度不为0而导致的,然而这个hash长度不为0,是由于问题3导致的,由于新老子应用切换时,新子应用进行了前后台的切换,导致了首次跳转的默认路由不再为‘/’,而是动态的上一次路由。之前设计没有子应用前后台切换的情况,而且首次‘/’的跳转我们做成的默认打开工作台的设定,所以没有问题。

解决方案:目前没有发现如果把(0, eval)('window').location.hash?.length > 2判断删除的话有什么问题,可以把这个判断去掉。

if ((0, eval)('window').location.hash?.length > 2 && !count) {
			return
		}

上线方案

由于子应用过多,先采取部分子应用测试,在测试环境试运行一段时间

详细说明:

1. 创建子应用白名单,主应用保留双边逻辑,白名单内的子应用走新逻辑,之外走老逻辑

2. 白名单内的子应用按照说明,进行改造即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值