前言
问题产生来源:项目整体微前端的框架,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。项目初期每个子应用的菜单均在本应用下,所以面包屑可以正常展示。中期客户突然提出想按照功能去划分菜单,导致菜单位置发生大面积变动。一开始感觉没什么,配权限的时候,按照相应的菜单位置去配置就好啦,前端没啥工作量啊。(想太轻易了....)后续发现的问题就是原本属于A应用下的B菜单,现在归属到了C应用。所以原本面包屑是A/B,现在应该得是C/B。而又因为面包屑的取值是在路由文件中,层级是子应用中的路由层级。所以展示出的还是A/B。
解决思路
举例为在A应用中的B菜单在C应用的解决方案,A应用中的B菜单在A应用同理
- 按照目前的应用菜单位置,梳理出一份面包屑配置文件-json格式,该文件需要放到基座应用中。
js
"id": "APP_C", //C应用的唯一标识
"name": "C应用", //C应用的名称
"children": [
{
"id": "APP_C_B", //C应用下的B菜单的唯一标识
"name": "B菜单", //C应用下的B菜单的名称
"url": "xxxxxxxx" //C应用下的B菜单的url地址
}
]
}
2.在A应用的路由文件中meta对象中id,此id应与配置文件中的id相同
js
const routeInfo = [
{
path: '*******',
meta: {
id: 'APP_C_B' //B菜单对应的id
},
//若还有下级可继续编码,面包屑配置文件同理
children: [
{
path: '*******',
meta: {
id: ''
},
children: []
}
]
}
]
export default routeInfo
3.在A应用的路由文件中添加全局前置守卫,通过该守卫A应用向基座应用发送数据。
js
const useRouterCreate = () => {
const router = createRouter({
history: createWebHashHistory()
})
router.beforeEach((to) => {
//子应用给基座发送数据
dispatchEvent({
url: `/****/****/#${to.fullPath}`,
id: to.meta.id
})
})
return router
}
export default useRouterCreate
4.在基座中的面包屑vue文件中,当监听到A应用给基座发送的数据后,通过判断A应用发送的id与面包屑的id做递归匹配,从而使得面包屑的层级按照配置文件中的层级去展示,达到C/B的效果。递归方法如下:
js
/**
*
* @param {*} tree 路由配置文件
* @param {*} targetUrl 匹配的id
* @param {*} parents 面包屑格式
* @returns
*/
function findNodeAndParent(tree, targetUrl, parents = []) {
for (const node of tree) {
// 将当前节点添加到父级数组中
const currentParents = [
...parents,
{ name: node.name, id: node.id, path: node.path, url: node.url }
]
if (node.id === targetUrl) {
// 找到目标节点,返回它和所有的父级组件
return currentParents
}
if (node.children) {
// 如果有子节点,递归搜索子节点
const result = findNodeAndParent(node.children, targetUrl, currentParents)
if (result) {
return result // 如果找到了,返回结果
}
}
}
return null // 如果未找到目标节点,返回null
}
发现的问题
因为micro-app支持不同框架的子应用,所以整个系统不仅包含了新开发的vue3项目,还嵌套了vue2的老项目,问题就出现在了vue2这些老项目中,首次进入vue2项目的某一菜单时,面包屑加载不出来,刷新浏览器也不行,但是再点击该项目中的另一个菜单时,就会加载出来了。总结来说就是初次加载vue2项目时,面包屑加载不出来,再次点击该项目下的其他菜单时,就都可以正常展示了。
排查问题
初步怀疑是加载顺序导致的,于是从main.js开始排查
js
const appElId = '#子应用id';
const mount = () => {
app = new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount(appElId, true);
//问题出现在了这里
initCommunicate(appElId);
// 路由处理
addDataListener((data) => {
i18n.mergeLocaleMessage(data.language, data.common);
// 通过基座应用通知子应用进行路由跳转
if (data.path && typeof data.path === 'string') {
const path = data.path.replace(/^#/, '');
// 当基座下发path时进行跳转
if (path && path !== router.currentRoute.path) {
router.push(path);
}
}
}, true);
};
js
let microSubApp = null;
/*
* 是否是子应用实例
*/
function hasSubAppInstance() {
return !!microSubApp;
}
/**
* 初始化与基座应用的通信
* @param appRoot
* @return Boolean 是否初始化成功
*/
export function initCommunicate(appEl) {
const appName = getAppName(appEl);
const subApp = window[`__SUB_APP__${appName || ''}`] || window.microApp;
if (subApp) {
microSubApp = subApp;
}
return hasSubAppInstance();
}
发现原来当挂载子应用时,此时并未初始化与基座应用的通信,导致初始化未成功。此时函数hasSubAppInstance()的返回值为false。
js
/**
* 向基座应用发送数据
* @param event 被发送的数据
*/
export function dispatchEvent(event) {
if (hasSubAppInstance()) {
//未执行
microSubApp.dispatch(event);
}
}
导致全局前置守卫中,dispatchEvent内部并未执行,所以没有将子应用的数据发送至基座,基座未接收到数据,导致面包屑未展示。
点击其他菜单的时候,此时已经初始化子应用与基座应用的通信,子应用可以正常的向基座发送数据,所以面包屑就可以成功展示了。