微前端架构实战:从入门到落地的完整指南

Top丶程哲 框架 阅读 2,114
赞 12 收藏
二维码
手机扫码查看
反馈

为啥要搞微前端?

去年接手一个老后台系统,技术栈是 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,但感觉不够优雅。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论