微前端架构实战:从入门到落地的完整指南
为啥要搞微前端?
去年接手一个老后台系统,技术栈是 Vue 2 + Element UI,跑得好好的,但业务部门突然要加一堆新功能,而且要求用 React 重写。我一听就头大——全盘重写成本太高,不重写又没法用新框架。最后拍板:上微前端,主应用保留 Vue,子应用用 React 搞。
选型时对比了 qiankun、single-spa、Module Federation,最后选了 qiankun。原因很现实:文档全、社区活跃、我们团队没人玩过微前端,得选个“踩坑有人填”的。
搭起来其实没那么难
主应用(Vue)装个 qiankun,注册子应用:
// main.js (主应用)
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react-app',
entry: '//localhost:3001', // 子应用开发服务器
container: '#subapp-viewport',
activeRule: '/react',
},
]);
start();
子应用(React)导出三个生命周期函数:
// src/microApp.js (子应用)
export async function bootstrap() {
console.log('react app bootstraped');
}
export async function mount(props) {
render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
export async function unmount() {
const root = document.getElementById('root');
if (root) {
unmountComponentAtNode(root);
}
}
本地开发时,主应用 proxy 一下子应用的静态资源,或者直接用 devServer 的 publicPath。这部分文档写得挺清楚,照着做基本能跑起来。
最大的坑:样式污染和全局变量冲突
跑起来第一天就炸了。子应用的 Ant Design 样式把主应用的 Element UI 按钮全干趴了。更惨的是,两个应用都用了 window.utils,互相覆盖,主应用的工具函数被子应用干掉,直接报错。
一开始想用 CSS Modules,但子应用是全新项目还好办,主应用是老系统,改起来要命。后来试了 qiankun 的 sandbox: { strictStyleIsolation: true },结果发现这玩意儿是用 Shadow DOM 实现的,IE 直接挂掉,而且某些第三方组件(比如 ECharts)在 Shadow DOM 里渲染异常,图表空白。
折腾半天,最后妥协方案:
- 主应用和子应用的 class 命名加前缀,比如主应用
.main-btn,子应用.sub-btn,靠约定避免冲突 - 全局变量统一挂到
window.__MICRO_APP__下,比如window.__MICRO_APP__.utils = {...},主应用访问时也走这个路径
虽然土,但有效。后来还加了个 ESLint 规则,禁止直接写 window.xxx,必须通过封装函数访问,减少手滑。
性能问题差点让我背锅
上线前压测,发现切换子应用时白屏 1.5 秒。查了下,每次进子应用都要重新加载 JS/CSS,哪怕之前已经加载过。用户在主应用和子应用之间来回切,体验极差。
qiankun 官方说支持 prefetch,但默认只在空闲时预加载,我们业务场景是“用户点菜单就进子应用”,根本等不到空闲。后来手动改了注册逻辑,把子应用的 entry 提前 fetch 一次,缓存到内存:
// 主应用中预加载子应用资源
const cachedEntries = new Map();
function preloadApp(entry) {
if (cachedEntries.has(entry)) return cachedEntries.get(entry);
const script = document.createElement('script');
script.src = ${entry}/main.js;
script.async = true;
document.head.appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = ${entry}/style.css;
document.head.appendChild(link);
cachedEntries.set(entry, { script, link });
}
// 注册前先预加载
preloadApp('//localhost:3001');
registerMicroApps([...]);
这样第二次进入子应用时,资源直接从缓存走,白屏时间降到 300ms。不过要注意内存泄漏,用户长时间不操作得清理缓存,我们加了个 5 分钟自动清理的定时器。
路由同步的骚操作
主应用用 Vue Router,子应用用 React Router,两个 history 独立管理。用户在子应用里点浏览器后退,主应用的菜单高亮没变,体验割裂。
最开始想用 qiankun 的 props 传主应用的 router 实例给子应用,让子应用主动同步。但子应用团队不愿意改代码,说“我们是独立应用”。最后我在主应用里监听全局 popstate 事件,手动触发子应用的路由更新:
// 主应用
window.addEventListener('popstate', () => {
// 如果当前在子应用路由下,通知子应用更新
if (window.location.pathname.startsWith('/react')) {
window.dispatchEvent(new CustomEvent('micro-app-popstate'));
}
});
// 子应用
window.addEventListener('micro-app-popstate', () => {
// 强制 React Router 重新读取当前 URL
history.push(window.location.pathname + window.location.search);
});
有点 hack,但确实解决了问题。后来发现 qiankun 2.0+ 有 activeRule 支持函数,可以更精细控制,但当时项目赶时间,没升级。
回顾与反思
整体来说,微前端让我们在不重写老系统的情况下,顺利接入了新业务。但代价也不小:
- 调试复杂度飙升,主子应用联调要开两个服务,source map 经常对不上
- 部署流程变麻烦,主应用和子应用要分别构建、分别部署,还得保证版本兼容
- 有些问题至今没完美解决,比如子应用的 WebSocket 连接在 unmount 时偶尔没断开,导致内存泄漏(不过影响不大,每天凌晨重启服务)
如果现在重来,我会更谨慎评估是否真的需要微前端。对于新项目,可能 Module Federation 更合适;对于纯展示型子应用,iframe 其实更省事。微前端适合那种“多个团队独立开发、技术栈不同、必须集成到一个入口”的场景,别为了用而用。
对了,还有一个小技巧:子应用的 publicPath 动态设置。本地开发和线上环境路径不同,直接在子应用入口加这段:
// 子应用 publicPath 动态适配
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
不然静态资源会 404,这个坑我踩了两次才记住。
最后说两句
以上是我踩坑后的总结,希望对你有帮助。微前端不是银弹,但特定场景下确实能救命。如果你也在搞类似项目,建议先拉个最小原型验证核心问题(比如样式隔离、通信),别一上来就往生产环境怼。
有更优的实现方式欢迎评论区交流,比如你们怎么处理全局状态共享的?我们目前用的是主应用提供 props 传 store,但感觉不够优雅。

暂无评论