浅聊一下最近使用微应用micro-app踩到的坑

前言

问题产生来源:项目整体微前端的框架,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。项目初期每个子应用的菜单均在本应用下,所以面包屑可以正常展示。中期客户突然提出想按照功能去划分菜单,导致菜单位置发生大面积变动。一开始感觉没什么,配权限的时候,按照相应的菜单位置去配置就好啦,前端没啥工作量啊。(想太轻易了….)后续发现的问题就是原本属于A应用下的B菜单,现在归属到了C应用。所以原本面包屑是A/B,现在应该得是C/B。而又因为面包屑的取值是在路由文件中,层级是子应用中的路由层级。所以展示出的还是A/B。

解决思路

举例为在A应用中的B菜单在C应用的解决方案,A应用中的B菜单在A应用同理
  1. 按照目前的应用菜单位置,梳理出一份面包屑配置文件-json格式,该文件需要放到基座应用中。
        "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相同

const routeInfo = [
  {
    path: '*******',
    meta: {
          id: 'APP_C_B'   //B菜单对应的id
        },
        //若还有下级可继续编码,面包屑配置文件同理
    children: [
      {
        path: '*******',
        meta: {
          id: ''
        },
        children: []
      }
    ]
  }
]
export default routeInfo

3.在A应用的路由文件中添加全局前置守卫,通过该守卫A应用向基座应用发送数据。

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的效果。递归方法如下:

/**
 *
 * @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开始排查

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);
};
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。

/**
 * 向基座应用发送数据
 * @param event 被发送的数据
 */
export function dispatchEvent(event) {
  if (hasSubAppInstance()) {
    //未执行
    microSubApp.dispatch(event);
  }
}

导致全局前置守卫中,dispatchEvent内部并未执行,所以没有将子应用的数据发送至基座,基座未接收到数据,导致面包屑未展示。

浅聊一下最近使用微应用micro-app踩到的坑
点击其他菜单的时候,此时已经初始化子应用与基座应用的通信,子应用可以正常的向基座发送数据,所以面包屑就可以成功展示了。

原文链接:https://juejin.cn/post/7345097595528953868 作者:冰鲜柠檬水

(0)
上一篇 2024年3月13日 上午10:52
下一篇 2024年3月13日 上午11:03

相关推荐

发表回复

登录后才能评论