前言
对于大型前端项目,尤其是中后台项目,微前端架构已经成为越来越广泛的技术选型。笔者所在团队负责公司内部运营平台的开发和维护,大大小小的项目有7、8个。这些项目有运行了5年的"老"项目,也有配合业务需要搭建的新项目。为了统一管理,笔者逐步将这些项目纳入到monorepo
单仓中,并配合微前端重构整个平台,解决项目之间独立开发时代码冲突、发布冲突、规范不一致等问题。
在考察了社区优秀的微前端方案后,决定选择qiankun作为平台的微前端化技术选型。微前端应用的好处qiankun
官方文档已经说了很多了,这里讲讲它的痛点:
- 部署复杂度变大:对微应用的部署有两种模式:容器化部署 or CDN静态部署 ,无论哪种都需要大量额外的配置以便主应用能正确加载(比如开启CORS)。
- 微应用加载性能下降:这算是有舍有得吧,毕竟之前只需要加载一个应用,改为微前端后可是要加载主应用+微应用。
- 调试麻烦:主应用还好,启动起来就OK,微应用的调试就费劲了,不仅需要启动自己,还得启动主应用。
痛点1、2基本没啥办法,想用微前端就忍忍吧。痛点3不想忍,能不能优化一下调试方式,让微应用可以丝滑地进行调试呢?
方案设想
微应用调试的有三大痛点:
- 一些数据需要主应用传递,此时就需要同时启动主应用才能进行调试;
- 微应用独立启动时无法判断主、微应用样式是否冲突;
- 接口代理比较麻烦
这些痛点有两点是跟主应用相关,最后一点也是主应用存在并解决了的。那能不能利用生产模式(即打包后部署到服务器上进行访问的模式)下的主应用来调试微应用呢?
来看一下主应用里注册微应用的方法:
js
// qiankun官网里的示例代码
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
其中entry
便是微应用的服务地址,是提前声明的...能不能改为动态声明 ,动态读取 ?
让在生产模式下主应用动态地注册微应用,并且注册的地址可以让开发者自己配置,比如主应用部署域名是www.main-app.com
,微应用本地启动地址为localhost:3000
。将微应用的本地地址声明为entry的值,是不是就可以让生产模式的主应用加载本地启动的微应用。
思路有了,接下来便是实施!
方案实施
动态注册实现思路
动态注册的第一层含义是微应用信息列表不是写死在代码里的,而是动态读取的。
从哪里读呢?一个store里,该store还得支持按照开发者进行数据隔离,并能自由地读写。再叠加持久化要求,localStorage是个合适的存储方案。
动态注册的第二层含义是要支持开发者自定义微应用entry
的值,这个好办,在web页面里开发一个配置入口,让开发者可以自主设置值。
微应用注册表改造
首先需要声明一个默认注册表,为默认模式下微应用的配置信息,并且该注册表在实际返回时可以根据开发者的输入动态修改:
js
// micro-app.ts文件
const S_KEY = "QIANKUN_MICRO_DEV_MODE";
export const MICRO_APP_CONTAINER = "#micro-frontend-root";
const defaultMicroAppConfig = [
{
name: "react app", // app name registered
entry: "//localhost:3001",
container: "#micro-frontend-root",
activeRule: "/react-app",
},
{
name: "vue app",
entry: "//localhost:3002",
container: "#micro-frontend-root",
activeRule: "/vue-app",
},
];
const getFinalMicroAppConfig = () => {
const storage = window.localStorage;
try {
const itemStr = storage.getItem(S_KEY) ?? "";
let config: any;
if (itemStr) {
config = JSON.parse(itemStr);
}
if (typeof config === "object") {
return defaultMicroAppConfig.map((e) => {
const appName = e.name ?? "";
const devConfig = config?.[appName] ?? {};
if (devConfig.devSwitch) {
return {
...e,
entry: devConfig.url ?? e.entry,
};
}
return e;
});
}
return defaultMicroAppConfig;
} catch (e: any) {
console.error(e?.message);
return defaultMicroAppConfig;
}
};
// TODO 生产环境可选直接返回defaultMicroAppConfig, 屏蔽microDev模式
const FINAL_MICRO_APP_CONFIG = getFinalMicroAppConfig();
export default FINAL_MICRO_APP_CONFIG;
上面的代码逻辑很简单,就是在返回defaultMicroAppConfig
前判断localStorage
里有没有自定义的微应用配置,如果有则用自定义配置替换掉默认的配置。
动态配置微应用弹框
为了方便开发者配置微应用入口,需要在主应用里提供一个弹框组件,让开发者以可视化的方式设置要调试的微应用entry:
js
import React, { useState, useEffect, useMemo, useReducer, useCallback } from 'react';
import { Dialog } from 'tdesign-react';
import { useKeyPress } from 'ahooks';
import { handleKeyCode } from './utils';
import MicroFeTable from './components/micro-fe-table';
import MicroFePopup from './components/micro-fe-popup';
import { Provider, contextReducer, defaultContext, ACTION } from './context';
import useMicroDevData, { S_KEY } from './hooks/use-micro-dev-data';
export { S_KEY };
/**
* 微前端联调模式组件
* @returns
*/
export default function MicroDevMode(): React.ReactElement {
const [visible, setVisible] = useState(false);
const [data,,saveData] = useMicroDevData();
const [currentContext, dispatchContext] = useReducer(contextReducer, defaultContext);
const microDevApp = useMemo(() => currentContext?.configData?.filter(e => e.devSwitch), [currentContext?.configData]);
useEffect(() => {
(window as any).microDev = (open: any = undefined) => {
setVisible(open === undefined ? true : Boolean(open));
};
}, []);
useEffect(() => {
if (data) {
dispatchContext({
type: ACTION.SAVE_CONFIG_DATA,
data,
});
}
}, [data]);
useKeyPress(
handleKeyCode('KeyM'),
() => {
setVisible(prev => !prev);
},
);
const closeModal = useCallback(() => {
setVisible(false);
}, []);
const openModal = useCallback(() => {
setVisible(true);
}, []);
const onConfirm = () => {
const { configData } = currentContext;
if (configData) {
saveData(configData);
}
window.location.reload();
closeModal();
};
const onCancel = () => {
closeModal();
};
return (
<Provider value={{ ...currentContext, modalVisible: visible, dispatchContext }}>
{microDevApp?.length ? <MicroFePopup count={microDevApp?.length} onClick={openModal} /> : null}
<Dialog
header="Micro Dev"
width={800}
visible={visible}
confirmOnEnter
onConfirm={onConfirm}
onCancel={onCancel}
onEscKeydown={onCancel}
onCloseBtnClick={onCancel}
onOverlayClick={onCancel}
>
<MicroFeTable />
</Dialog>
</Provider>
);
}
上面列出了主入口代码,用一个Dialog
展示配置列表,并且提供两种方式弹出该配置框:
- 在浏览器
Console
调用microDev()
- 快捷键:
ctrl+shift(macos系统下为option键)+M
弹框效果: 上面的应用状态是起到提示效果,让开发者确认当前服务状态,以防没有启动本地服务就修改了配置,造成微应用加载失败。
如何检测服务健康度
很简单,在浏览器端发起一次http嗅探请求,当请求响应码为200
时,应用状态正常,否则异常
js
fetch(url)
.then(({ statusText, status }) => {
if (status === 200 && statusText === "OK") {
setTagMeta({
theme: "success",
text: "正常",
});
} else {
setTagMeta({
theme: "danger",
text: "异常",
});
}
})
.catch(() => {
setTagMeta({
theme: "danger",
text: "异常",
});
});
如何让动态配置生效
一般来说,主应用注册微应用的时机是在应用入口。所以为了更新微应用的配置,只能重新加载页面:
js
const onConfirm = () => {
const { configData } = currentContext;
if (configData) {
// 保存配置到localStorage
saveData(configData);
}
// 重新加载页面
window.location.reload();
closeModal();
};
效果
为了演示本地联调效果,故意将微应用初始entry设置一个不存在的服务,然后通过本地联调模式调用localhost:3001
地址下的微应用,页面刷新后,微应用被正确加载:
总结
本文通过设计microDev
模式,解决了微前端框架微应用调试的痛点,需要注意的是,该模式存在一定安全风险,最好在非正式环境使用(比如内部测试环境)。
附录
本文的demo项目代码已上传到github,需要完整代码的读者可以自行clone:qiankun-micro-dev