微前端落地,悬着的心终于放下了
「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言
产品提出系统要模块化,通过不同的模块组合构建我们的应用。这不是最近很火的微前端吗,和组内的小伙伴经过一番沟通,带着压力开始搞起来了,经过三个月的迭代开发,在业务开发的同时,把两个模块迁移成了子应用。系统上线了,悬着的心终于落下了。
现有的系统状况分析
现有的系统是一个独立的项目,采用umi框架开发,通过文件夹区分各大业务模块。公共组通过npm包的方式去管理。由于系统迭代时间比较久,架构上遗留的几个问题需要解决
子应用采用ant4.x
(antd用的3.x版本,升级antd4.x难度比较大,主要是Form变动比较大)hooks替换redux部分state
(redux stroe中的state差不多有一百多个,一些本不应该放在Redux里的状态也放到了里面)统一封装requst
(拆分成独立http和socket请求库,支持hooks)
第一个落地版本的架构
引入微前端要解决的问题
- 封装
脚手架
- 组件库
升级
,支持antd4.x
- 主子应用
通信
- 主应用
共享
一些实例
供子应用使用 - Redux Store得
部分数据
需要共享
,怎么做限制 - 子应用支持国际化,国际化内容
动态加载
- 子应用
跳转
父应用 - 样式隔离
改造正式开始
微前端框架用qiankun, 由于我们用的框架是umi, 对应提供了插件 @umijs/plugin-qiankun。
主应用
- 在umi配置文件开启qiankun配置
qiankun: {
master: {},
},
- 加载子应用配置
function microApp(entryPrefix) {
return [
{
name: 'app1', //唯一 id
entry: entryPrefix ? `${entryPrefix}/app1` : '//localhost:3000',
},
{
name: 'app2', //唯一 id
entry: entryPrefix ? `${entryPrefix}/app2` : '//localhost:3001',
},
... //其它子应用
];
}
export default microApp;
- 配置激活子应用路由
const router = [
{
path: '/main/app/rouer/app1', // (/main/app/rouer/)主应用部分路由
microApp: 'app1',
microAppProps: {
autoSetLoading: true,
className: 'appClassName',
wrapperClassName: 'wrapperClass',
},
},
{
path: '/main/app/rouer/app2',
microApp: 'app2',
microAppProps: {
autoSetLoading: true,
className: 'appClassName',
wrapperClassName: 'wrapperClass',
},
}
... //其它子应用
];
export default router;
- 在入口文件
app.js
导出qiankun对象
import microApp from './microApp';
import router from './router';
export const qiankun = new Promise((resolve) => {
const entryPrefix = process.env.NODE_ENV === 'production' ? window.location.origin : null;
const res = microApp(entryPrefix);
resolve(res);
}).then(apps => {
return {
apps,
routes: router,
};
});
子应用
- 在umi中开启qiankun配置
qiankun: {
slave: {},
},
- 在入口文件
app.js
导出qiankun的生命周期
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
init(props);
},
// 应用 render 之前触发
async mount(props) {
},
// 应用卸载时触发
async unmount(props) {
},
};
基于qiankun和umi提供的qiankun插件还是比较方便的运行起来。下面我们一起看下上面的问题怎么解决。
解决实际遇到的一些问题
组件库升级,支持antd4.x
我们采用施渐进式
重构,我们需要一种增量升级
的能力,先让新旧代码和谐共存
,再逐步转化旧代码,直到整个重构完成。目前我们把基于antd3.x
版本的组件库升级到4.x
。两个版本同时更新一段时间,两个版本可以共存
,迁出来的子应用使用antd 4.x
,保证新的特性我们能使用
主子应用通信
umi qiankun plugin
提供了主子应用通信的方式
在主应用入口导出useQiankunStateForSlave
方法
export function useQiankunStateForSlave() {
const [masterState, setMasterState] = useState({});
return {
SlaveSDK,
getStore,
// masterState,
setMasterState,
};
}
子应用中会自动生成一个全局 model,可以在任意组件中获取主应用透传的 props 的值。
import { useModel } from 'umi';
function MyPage() {
const masterProps = useModel('@@qiankunStateFromMaster');
return <div>{JSON.stringify(masterProps)}</div>;
}
该方式主子应用通信没问题,嵌套多层子应用使用不是很方便,后期我们会对它进行改造
主应用共享一些实例供子应用使用
在微前端架构当中,不推荐应用共享实例,因为这样子应该很难做到独立开发,独立部署,应用会有耦合。但由于我们是对系统做拆分,一些公用的功能,比如弹框查看日志,数据预览等功能会先用父应用的功能,需要共享。这里我们封装了SDK供子应用使用。后期如果要改造这块成独立开发,独立部署。我们实现对应的接口,动态注入,就可以实现。
- 定义log要共享的方法
class Log {
show = (params) =>
// 具体业务逻辑
}
showOther = (params) => {
// 具体业务逻辑
}
}
export default new Log();
- 在主应用中创建
SlaveSDK
实例
import intl from 'utils/intl';
import { history } from 'umi';
import { getParams } from 'utils/util';
import Log from './log';
class SlaveSDK {
constructor() {
this.log = Log;
this.intl = intl;
this.router = {
history, // 主应用的history对象
getParams, // 获取主应用的参数
};
... 其它实例共享
}
}
export default new SlaveSDK();
- 通过数据通信传递给子应用
import SlaveSDK from './SlaveSDK';
export function useQiankunStateForSlave() {
return {
SlaveSDK,
};
}
- 子应用在生命周期里获取sdk并初始化保存
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
props?.SlaveSDK && setSDK(props.SlaveSDK);
},
};
Redux Store部分数据需要共享,怎么做限制
子应用业务有可能需要Redux Store的部分数据
,但我们不能把整个Store都暴露给子应用
,在应用使用的时候做些限制,这里我们用到了代理
对象去实现
const allowGet = { auth: '', user: '' }; // 需要共享的key
const state = getReduxStore();
const proxy = new Proxy(state, {
get(target, property) {
if (property in allowGet) {
return target[property];
}
return undefined;
},
});
return proxy;
};
子应用支持国际化,国际化内容动态加载
我们通过共享实例,通过SDK的方式后去获取国际化实例intl
,在子应用初始化的时候去加载国际化。
/**
* 国际化初始化
*/
const intlInit = async () => {
// load语言 文件
const result = await import('./locales');
const intl = await import('./utils/intl');
try {
moment.locale(intl?.default?.getIntlLang?.() || 'zh_CN');
intl?.default?.load(result.default);
} catch (error) {
console.error('intl error', error);
}
};
子应用跳转父应用
在子应用中跳转父应用的路由,也是通过把父应用的router的history
对象传递给子应用
import { history } from 'umi';
class SlaveSDK {
constructor() {
this.router = {
history, // 主应用的history对象
getParams, // 获取主应用的参数
};
}
}
样式隔离
样式隔里子应用我们用的是cssModule
,编译的时候会自动生成唯一的key。主要问题是antd,因为我们既加载了antd3,又加载了antd4,导致样式会有冲突。我在antd4的编译的时候改前缀。配置如下
import { ConfigProvider } from 'antd';
// 弹框的前缀配置
ConfigProvider.config({
prefixCls: 'my-ant',
});
// 组件的配置
<ConfigProvider prefixCls="my-ant">
</ConfigProvider>
部署
系统采用nginx部署,是在同一个Server下通过不同的path来处理。配置如下
server {
listen 8080;
server_name localhost;
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /app1 {
root html;
index index.html index.htm;
try_files $uri $uri/ /app1/index.html;
}
location /app2 {
root html;
index index.html index.htm;
try_files $uri $uri/ /app2/index.html;
}
}
思考
目前第一版微前端已经上线,下个版本有可能会有嵌套多层子应用。以下问题需要解决
- 资源隔离
- 嵌套多层应用共享状态
- webpack5 Module Federation和qiankun结合做资源隔离
- 随着子应用的增多,脚手架增强
结束语
这个版本,脚手架完成了基础的功能,能根据模块去生成我们的子应用。后期增强之后,能生成主应用和子应用,会分享这块。以上是微前端落地这块总结的一些问题,如有问题,欢迎指正。
参考
如果你觉得该文章不错,不妨
1、点赞,让更多的人也能看到这篇内容
2、关注我,让我们成为长期关系
3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章
转载自:https://juejin.cn/post/6986834120290598942