前端项目基于qiankun 微前端改造记录

237 阅读2分钟

作者:戴荣兴

  1. 项目背景

图书集成信息平台目前数十个子系统,目前的实现方式是在一个子系统选择页跳转对应的子系统,如果用户需要在不同子系统操作,得重复切换子系统。此方式就有点繁琐,所以将多个子系统资源整合在一个统一的系统中就很有必要。

  1. 实现方案

  1. 具体需求

  • 实现后需要满足如下效果:
    • 页面的功能和展示效果需要保持和原来一致。
    • 改造的子系统的资源需要在一个菜单中显示。
    • 新旧版能够随时切换。
    • 如果用户不包含改造了的子系统资源,不应该跳转新界面。
    • 已经接入了qiankun的子应用需要不受影响。
  1. 技术选型

vue3+qiankun+antd-vue

  1. 具体实现

      1.   系统资源处理

    •   原有系统的菜单路由名称和层级关系是由前端维护的,统一工作项目中就需要把这部分关系在运维中心的资源管理处维护。在运维系统-资源管理增加了父级菜单和菜单是否隐藏2个字段,并且维护了排序值和页面名称这2个原有字段。后续资源需要按照前端菜单层级关系来新增。
    • {
          "id"1"name": "主系统",
          "children": [
              {
                  "id": 2,
                  "name": "产品开发管理系统",
                  "path": "pdm",
                  "children": [
                      {
                          "id": 21,
                          "name": "B单管理",
                          "path": "/module/pdm/sampleManage"
                          "children": [
                              {
                                   "id": 211,
                                   "name": "打样任务查询",
                                   "path": "/module/pdm/sampleManage/sampleSearch"
                              }
                          ]   
                      },
                      {
                          "id": 22,
                          "name": "A单管理",
                          "path": "/module/pdm/royaltyReview"
                           // ... 其他子级资源
                      }
                  ]
              },
              {
                  "id": 3,
                  "name": "销售BI系统",
                  "children": [
                      // ... 其他子级资源
                  ]
              }
          ]
      }
      
      1.   主应用搭建

    •   使用vue-cli搭建项目,生成下图所示项目:
    • api:系统http请求相关
    • assets:静态资源相关(图片、字体、全局样式等)
    • components:公共组件
    • global:引入全局方法、组件、样式等
    • hooks:自定义的hooks
    • layout:布局组件,包含左侧菜单和页面顶部组件
    • micro:子系统相关配置,及注册微前端方法
    • router:路由相关,其中router-guards.js是用来处理路由拦截的
    • store:状态管理,本次使用pinia
    • utils:公共方法
    • views:页面组件
    •     代码的重点是在micro和store目录:micro注册微前端相关的代码,store/app/index里处理子应用资源相关的代码。
    • // micro/index.js
      export  function  createMicroApps() {
          // 只能注册一次
          if ( isRegister) return ;
          isRegister = true ;
          const appStore = useAppStoreWithOut();
          
          // 根据配置生成子应用列表
          let apps = microName.map((el) => {
              return {
                  name: el.micro,
                  // entry: el.micro == 'sell' ? 'http://localhost:8086/' : el.path + '/',
                  entry: el.path + '/',
                  container: '#microContainer',
                  activeRule: getActiveRule(`#${el.modules}`),
                  props: {
                      container: '#microContainer',
                      token: getToken(),
                  },
              };
          });
      
          // 数据字典系统另做处理
          apps = apps.concat({
              name: 'dataDic',
              entry: isProduct ? 'https://*********/dataDic/' : 'http://test******/dataDic/',
              container: '#microContainer',
              activeRule: getActiveRule('#/dataDic'),
              props: {
                  container: '#microContainer',
                  token: getToken(),
              },
          });
      
          registerMicroApps(apps, {
              beforeLoad: [
                  (app) => {
                      appStore.setQiankunLoading(true ) ;  // 资源加载时的loading
                  },
              ],
              beforeMount: [
                  (app) => {
                      appStore.setQiankunLoading(false ) ;
                  },
              ],
              afterUnmount: [
                  (app) => {
                      appStore.setQiankunLoading(false ) ;
                  },
              ],
          });
      }
      
    • // store/modules/app/index.js
      // 登录后根据当前是否新老系统,做不同的处理
      // 首次登录根据当前角色所拥有的的资源来判断是否跳转新的界面:如果其资源不包含在本次改造的子系统中,则跳转manage页面,反之直接跳转新界面
      // 下次登录则取本地存储的跳转方式
      // systemType: '1' 新的界面
      // systemType: '2' 旧的manage界面
      // isAuto用来控制是否需要同时加载4级资源。
      getResource(isAuto) {
          return  new Promise(async (resolve, reject) => {
              let systemType = getSystemType();
      
              if (systemType === '2') {
                  return resolve(systemType);
              }
      
              const { micro } = await  this.getSystemsList();
      
              if (isUndef(systemType) && micro.length === 0) {
                  setSystemType('2');
                  return resolve('2');
              }
      
              // 获取子系统资源
              const resources = await getMicroResources(micro);
              if (resources?.length === 0) {
                  setSystemType('2');
                  return resolve('2');
              }
      
              let asyncResources = resources.map((el, index) => {
                  let routes = [];
                  let system = micro[index];
                  if (el?.data?.status === 200) {
                      routes = el.data.items;
                  }
                  return {
                      routes: routes,
                      system: system,
                  };
              });
      
              if (isAuto) {
                  await  this.getResourceLevel4(micro, asyncResources);
              } else {
                  this.getResourceLevel4(micro, asyncResources);
              }
              this.microList = createTreeResources(asyncResources);
              setResources(this.microList);
              return resolve('1');
          });
      },
      
    •     在layout/microContainer.vue文件中增加子应用容器,在组件挂载之后加载子应用。这个组件需要一直存在,所以在代码中用v-show来控制显示隐藏。相关问题
    • // prefetch: false,去掉预加载
      // sandbox开启样式隔离,加上改配置后,会在子系统的css选择器前面加上
      // div[data-qiankun="子应用name"]
      start({ prefetch: false, sandbox: { experimentalStyleIsolation: true } });
      
      1.   子系统处理

    •   在主应用中,给资源统一加上了系统前缀来区分各个子系统,为了是子系统的路由匹配生效,在各个子系统中需要加上路由前缀。以下用基础数据项目为例:
    • 资源处理
    • // router/index.js
      
      // 给路由增加前缀的方法
      export  function routerBaseUrl(list) {
          if(!isQiankun) return;
          list.forEach(item=>{
              let path = item.path;
              let redirect = item.redirect;
              if(path && path.indexOf('/')===0){
                  item.oldPath = path;
                  item.path = '/module/baseData' + path;
              }
      
              if(redirect && redirect.indexOf('/')===0){
                  item.oldRedirect = redirect;
                  item.redirect = '/module/baseData' + redirect;
              }
      
              if(item.children && item.children.length){
                  routerBaseUrl(item.children)
              }
          })
      }
      
      // 重新设置子系统layout组件,作为子应用时不需要左侧菜单和顶部header
      function setLayout(list) {
          if (!isQiankun) {
              return;
          }
      
          list.forEach((item) => {
              if (item?.component?.name === 'layout') {
                  item.component = microLayout;
              }
      
              if (item?.children?.length) {
                  setLayout(item.children);
              }
          });
      }
      
    • 跳转处理
    • // 需要处理三种情况的跳转
      // 1. vue-router跳转
      function nextPath(next, path) {
          if (isQiankun) {
              if (typeof path === 'object' && !path.path.includes('/module/baseData')) {
                  path.path = `/module/baseData${path.path}`;
                  next(path);
                  return;
              }
              if (typeof path === 'string' && !path.includes('/module/baseData')) {
                  next(`/module/baseData${path}`);
                  return;
              }
          }
          next();
      }
      
      // 2. window.open跳转
      (function () {
          if (isQiankun) {
              let winOpen = window.open;
              window.open = function () {
                  let arg = [...arguments];
                  let url = arg[0];
                  let isRouterPath = arg[1];
                  if (isRouterPath == 'micro' && url?.startsWith('#')) {
                      url = `#${url.replace('#', '/module/baseData')}`;
                      winOpen(url);
                  } else {
                      winOpen(...arg);
                  }
              };
          }
      })();
      
      // 3. a标签获取vue-link标签打开新界面跳转
      Vue.prototype.$_qiankun = function(link) {
          if (!isQiankun || !link || link.includes('/module/baseData')) return link;
          let str = '/module/baseData';
          if (link.includes('#')) {
              str = '/#/module/baseData';
          }
          return link.replace(/#?/, str);
      };
      
    • 导出子应用钩子函数
    • let instance = null ;
      const  render = (e) => {
          if ( isQiankun) {
              const { container } = e;
              instance = new  Vue({
                  el: container ? container.querySelector('#app') : '#app',
                  router,
                  store,
                  template: '<App/>',
                  components: { App },
              });
          } else {
              const { appContent, loading } = e;
              if ( !instance) {
                  instance = new  Vue({
                      el: '#app',
                      router,
                      store,
                      template: '<App/>',
                      components: { App },
                  });
              } else {
                  // if (!appContent) return;
                  store.commit('SET_APP_CONTENT', appContent);
                  store.commit('SET_APP_LOADING', loading);
              }
          }
      };
      
      if ( !isQiankun) {
          //第一次调用初始主应用
          render({});
      
          const  genActiveRule = (routerPrefix) => {
              return  ( location) => location.hash.startsWith(routerPrefix);
          };
      
          let apps = [
              {
                  name: 'dataDic',
                  entry: process.env.VUE_APP_MICRO_URL,
                  render: render,
                  activeRule: genActiveRule('#/dataDic'),
                  props: {
                      token: getToken(),
                  },
              },
          ];
      
          //注册的子应用 参数为数组
          registerMicroApps(apps, {
              beforeLoad: [(app) => {}],
              beforeMount: [(app) => {}],
              afterUnmount: [(app) => {}],
          });
      
          start();
      }
      
      export  async  function  bootstrap() {
          console.log('baseData app bootstraped');
      }
      export  async  function  mount(props) {
          render(props);
          setToken(props.token);
      }
      export  async  function  unmount(props) {
          if ( instance) {
              instance.$destroy();
              instance.$el.innerHTML = '';
              instance = null ;
          }
      }