qiankun微前端功能迁移

505 阅读6分钟

需求背景

营销后台需要迁移另一个平台的功能,而且开发时间很紧。其实在开发时间限制之下,另一个项目是React的项目是React,而营销后台是vue项目。综合这两点,决定采用微前端的方式处理。

微前端可以不管两个项目的技术栈,而且两个项目都是后台布局是一致的,这样可以节省开发时间,主要开发时间在于微前端的配置,并且迁移过来的一同不用进行二次测试。

技术方案上选择了阿里的qiankun框架,因为qiankun已经使用了多年,配置稳定,而且遇到问题的话,官方的issue活跃度还可以

方案实现

主应用配置

主应用的技术栈: vue v3.x + ant-design-vue的方案,对于主应用的配置文件,需要关注: output的配置, dev-server的配置以及qiankun注册配置

项目打包配置 因为项目使用了packer构建工具,一些地方配置和会有区别,但是本质传递给webpack的配置部分还是一样的配置项

output: {
  library: `${packageName}-[name]`,
  libraryTarget: 'umd',
  chunkLoadingGlobal: `webpackJsonp_${packageName}`,
}

dev-server配置 在本地进行主/子应用联调的时候会遇到,proxy相关的错误,这时候需要配置dev-server相关配置,支持跨域请求

devServer: {
  proxy,
  port: 8880,
  client: {
    overlay: false,
  },
  // 配置微前端跨域处理
  headers: {
    'Access-Control-Allow-Origin': '*',
  }
}

qiankun注册配置 我会单独在src/目录下,新建一个micros的文件,专门存放微前端相关配置

image.png

因为采用qiankun的手动挂载的方案,所以配置和自动挂载略有区别

/**
 * name: 微应用名称 - 具有唯一性
 * entry: 微应用入口 - 通过该地址加载微应用
 * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
 * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
 */
export const triggerRuleMicroConfig = {
  name: 'business-admin-sub',
  entry: '/xxx/#/business-admin/tools-center/msg-rule',
  container: '#micros-app-trigger-rule',
  props: {
    current_brand: 1,
  },
};

在主应用单独新建一个页面,使用loadMicroApp手动挂载处理, 这里需要注意的是开启沙箱,避免主/子应用样式的相互影响,到这一步基本可以展示子应用页面,如果这一步无法展示,基本问题都是在子应用这边在index.tsx中,初始化render的这一步失败

<template>
  <div class="test-platform-container">
    <div id="micros-app-trigger-rule"></div>
  </div>
</template>

<script setup lang="ts">
import { triggerRuleMicroConfig } from '@/micros/apps';
import { loadMicroApp, MicroApp } from 'qiankun';
import { getDataFromURL } from '@utils/getData';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@store/userInfo';
const { getLocalStorage } = useLocalStorage();
let microApp: MicroApp | null = null;
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();

function settingPageAndBtnAuthList() {
  const customActionPermissionList = route?.meta?.actionPermissions.map((item) => {
    return `${route?.name}-${item}`;
  });
  const res = customActionPermissionList.reduce((preItem, currItem, currIndex) => {
    const isAuth = userStore.myCurRole.actionPermissions.indexOf(currItem) !== -1;
    if (isAuth) {
      const permissionKey = route?.meta?.actionPermissions[currIndex];
      preItem.push(permissionKey);
    }
    return preItem;
  }, []);
  return res.join(',');
}
onMounted(() => {
  // 设置主应用的authList
  const authListStr = settingPageAndBtnAuthList();
  const payloadProps = {
    token: getDataFromURL('usertoken') || getLocalStorage('qilin_user_token') || '',
    current_brand: 1,
    authListStr,
  };
  microApp = loadMicroApp(
    {
      ...triggerRuleMicroConfig,
      props: payloadProps,
    },
    {
      sandbox: {
        experimentalStyleIsolation: true,
      },
    }
  );
});
onBeforeUnmount(() => {
  if (microApp) {
    microApp.unmount();
  }
});
</script>

<style scoped lang="less">
.test-platform-container {
  min-height: 100vh;

  .micros-app-group {
    min-height: 100vh;
  }
}
</style>

子应用配置

子应用的技术栈: react v18.x + antd的方案,对于子应用来说,主要关注三件事: 项目打包配置、渲染配置、路由配置,只要这三项不出问题,是可以直接在主应用中显示子应用页面的,如果出现样式的问题,可以放在最后处理。

首先将carco的配置文件暴露出来,在carco.config.js中配置相关子应用的微前端配置, 主要是output相关配置,包括: library、libraryTarget\chunkLoadingGlobal这三项

webpack: {
  configure: (webpackConfig) => {
    webpackConfig.output.library = `${name}-[name]`;
    webpackConfig.output.libraryTarget = 'umd';
    webpackConfig.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
    return webpackConfig;
  },
},

然后配置src/index.tsx,需要将原来react的render的逻辑改写,具体可以参照官方文档, 这里一定要留一些console,方便主/子应用联调的时候,定位一些问题

import './utils/public-path'; //引入qiankun新增的 public-path 文件
import './wdyr';
import './index.css';
import { createRoot } from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import './assets/css/reset-ant-design.module.less';
import './assets/css/reset-ant-design.less';

let root: any = null;
// 渲染函数
const render = (props: { [x: string]: any; container?: any }) => {
console.log(' props -->-24', props);
const {
 container,
 token,
 brand,
 authListStr = '',
 actions: qiankunAction, onGlobalStateChange } = props;
if (token) {
 window.sessionStorage.setItem('token', token);
}
if (brand) {
 window.localStorage.setItem('current_brand', brand);
}
// 自定义处理authListStr
if (authListStr) {
 window.localStorage.setItem('authListStr', authListStr);
} else {
 // window.localStorage.removeItem('authListStr');
}
if (qiankunAction) {
 window.__qiankunActions = qiankunAction;
 window.__onGlobalStateChange = onGlobalStateChange;
}
// 设置popup等组件在微前端情况下的挂载处理
let customGetPopupContainer = container ? container.querySelector('#root') : (document.getElementById('root') as HTMLElement)
// @ts-ignore
window.__customGetPopupContainer = customGetPopupContainer;
root = createRoot(container ? container.querySelector('#root') : (document.getElementById('root') as HTMLElement));
root.render(
 <ConfigProvider locale={zhCN} getPopupContainer={() => customGetPopupContainer}>
   <App />
 </ConfigProvider>
);
};

if (!window.__POWERED_BY_QIANKUN__) {
render({});
}

export async function bootstrap() {
console.log('[react18] react app bootstraped');
}

export async function mount(props: any) {
console.log('[react18] props from main framework', props);
render(props);
}

export async function unmount(props: any) {
console.log('------ micro app unmount = ', props);
root.unmount();
root = null;
}

reportWebVitals();

路由配置修改,主要解决主应用挂载子应用对的时候,防止子应用的路由配置扰乱主应用的路由配置,导致页面乱跳重定向到其他路由页面。对于本次迁移来说比较方便的是,主/子应用都是hash路由,所以我们只需要处理路由的basename即可。 思路就是判断window对象是否含有qiankun特有标识,如果存在,则需要在当前路由配置上加上特定的basename,如果不存在,就是正常的路由配置即可

export const router = createHashRouter([
  {
    path: '/',
    element: <LayoutContainer />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      ...settings,
      ...salesTeam,
      ...toolsCenter,
      {
        path: '*',
        loader: async () => {
          return redirect('/');
        },
      },
    ],
  },
], {
  basename:window.__POWERED_BY_QIANKUN__ ? "/business-admin": "/"
});

其他问题处理

样式混乱的问题

如果开启了qiankun的沙箱,无论是严格沙箱还是基于shadowDOM的沙箱,样式基本都不会出现问题,但是有一种情况会出现问题,就是类似Modal,Message,Drawer等组件,不会页面加载的时候就渲染的组件,会出现样式丢失,遇到这种情况只需要记住一条原则: 哪个组件出现问题,就让出问题的组件配置新的父节点即可。

在开发调试的时候,你会发现微前端的挂载的节点都会在一层特定的shadowDOM下,如果样式出现了问题,基本就是相关DOM没有在这层DOM的包裹下,这时候需要配置antd的getContainer等组件特有的属性和ConfigProvider组件同时处理

let customGetPopupContainer = container ? container.querySelector('#root') : (document.getElementById('root') as HTMLElement)
// @ts-ignore
window.__customGetPopupContainer = customGetPopupContainer;
root = createRoot(container ? container.querySelector('#root') : (document.getElementById('root') as HTMLElement));
root.render(
  <ConfigProvider locale={zhCN} getPopupContainer={() => customGetPopupContainer}>
    <App />
  </ConfigProvider>
);
getContainer: () => window.__customGetPopupContainer,
主/子应用通信问题

微前端通信主要是两种props(只在页面挂载的时候生效,不会响应式)和actions通信,这里主要用了props的通信,但是actions的通信也做了处理,后续的时候已经使用,这里不再多说了,代码里都有体现

经验总结

由于这次的需求不是很复杂,没有涉及特别多的主/子应用的通信,所以基本难点都在于后续样式的处理,当前如果一些特殊场景选择了不开沙箱的模式,那就需要对于子应用的样式进行加自定义的特殊前缀处理,npm有很多这种包可以处理,算是一次难得的微前端整合尝试