需求背景
营销后台需要迁移另一个平台的功能,而且开发时间很紧。其实在开发时间限制之下,另一个项目是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的文件,专门存放微前端相关配置
因为采用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有很多这种包可以处理,算是一次难得的微前端整合尝试