qiankun微前端实战中的坑与最佳实践总结

上官艺菲 前端 阅读 1,322
赞 19 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个项目是个老系统改造,原来的几个独立后台要整合到一个统一的管理平台里。每个子系统都是不同团队维护的,技术栈也不一样,有 React 的、Vue 的,甚至还有一个是 jQuery 写的老古董。一开始我们想搞个 iframe 套着走天下,结果用户体验直接拉垮:滚动不连贯、通信麻烦、SEO 更别提了。

qiankun微前端实战中的坑与最佳实践总结

后来我就琢磨能不能用微前端。市面上的方案看了几个,single-spa 太底层,自己要处理一堆生命周期;wujie 轻量但社区小,出问题不好查。最后选了 qiankun,主要是文档还行,蚂蚁内部在用,感觉更稳一点。虽然我也知道这玩意儿坑不少,但当时觉得“总比 iframe 强”,就这么上了。

搭架子没那么难

主应用是 Vue3 + Vite 搭的,子应用各自启动,通过注册的方式挂载进来。qiankun 官方给了 webpack 的例子,但 Vite 的支持得自己配。折腾了一下午,把官方那个 vite-plugin-qiankun 插件加上,基本能跑起来。

主应用这边注册子应用是这么写的:

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react-app',
    entry: '//localhost:3001',
    container: '#subapp-container',
    activeRule: '/react',
  },
  {
    name: 'vue-app',
    entry: '//localhost:3002',
    container: '#subapp-container',
    activeRule: '/vue',
  },
]);

start({
  sandbox: {
    strictStyleIsolation: true,
  },
  fetch: (url, ...args) => {
    // 这里可以加拦截,比如代理静态资源
    return fetch(url, ...args);
  },
});

子应用那边也要改一点点,暴露生命周期钩子:

// vue 子应用 main.js
let instance = null;

export const bootstrap = () => {
  console.log('vue app bootstraped');
};

export const mount = (props) => {
  instance = createApp(App).mount('#app');
};

export const unmount = () => {
  instance?.unmount();
};

然后 Vite 配置里加个 base 和跨域:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { qiankunChild } from 'vite-plugin-qiankun';

export default defineConfig({
  plugins: [vue(), qiankunChild()],
  base: '/vue', // 和 activeRule 对应
  server: {
    port: 3002,
    cors: true,
  },
});

最大的坑:样式隔离和内存泄漏

本来以为搭完就能收工,结果一跑起来发现样式全乱了。React 子应用里的 antd 样式污染了主应用的按钮,反过来主应用的全局 CSS 也影响了子应用的布局。我一开始用了 strictStyleIsolation: true,结果发现某些动态插入的样式(比如 element-plus 的弹窗)根本不起作用——因为 shadow DOM 隔离太狠了,这些组件根本进不去。

后来改成 experimentalStyleIsolation: true,这个模式会在 class 名后加个 hash,相对温和一些。虽然不能完全杜绝冲突,但至少大部分场景稳了。这里注意我踩过好几次坑:子应用如果用了 css module 或者 scoped,一定要确保编译后的 class 不会被外部穿透。

另一个问题是切换子应用后,内存占用一直涨。打开 devtools 一看,Event Listener 挂着一堆没释放。排查半天发现是子应用里有些全局事件没解绑,比如:

mounted() {
  window.addEventListener('resize', this.handleResize);
  // 忘记在 unmount 里 remove
}

后来在 unmount 钩子里补上:

export const unmount = () => {
  window.removeEventListener('resize', this.handleResize);
  instance?.unmount();
};

但这事没法靠人肉检查,我们后来写了个 lint 规则,强制要求所有 addEventListener 都必须配对 remove。不过到现在还有偶尔漏掉的情况,只能靠 QA 发现了再修。

又踩坑了,路由跳转白屏

线上部署后遇到个诡异问题:从主应用跳 react 子应用,第一次进正常,刷新后白屏。控制台报错:Failed to fetch dynamically imported module。查了半天才发现是路径问题——生产环境子应用部署在不同的二级域名下,但打包时 publicPath 没设对。

解决方案是在子应用的入口文件顶部加一行:

__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ || '/';

这样 qiankun 会动态注入正确的资源路径。另外 Nginx 也要配好,让所有子应用的 HTML 都能正确返回,而不是 404。这块配置贴一下:

location /react/ {
  alias /var/www/react/;
  try_files $uri $uri/ /react/index.html;
}

location /vue/ {
  alias /var/www/vue/;
  try_files $uri $uri/ /vue/index.html;
}

通信这事,简单粗暴最有效

子应用之间要传用户信息,一开始我想搞个 event bus 或 context 上下文,后来发现太复杂。最后用了最土的办法:通过 props 传给子应用的 mount 函数。

// 主应用
start({
  props: {
    userInfo: getUserInfo(),
    onUserChange: (cb) => subscribeUser(cb),
  },
});

// 子应用 mount 时接收
export const mount = (props) => {
  const { userInfo, onUserChange } = props;
  // 存到 pinia 或 redux 里
};

虽然不够“优雅”,但稳定,而且调试方便。qiankun 提供的 initGlobalState 其实也能用,但我试了发现有时候事件丢失,可能是沙箱环境的问题,就没敢上生产。

性能其实还能忍

首屏加载确实慢了点,毕竟要等所有子应用的 manifest 文件都拉下来。我们做了按需加载——只有访问对应路由才注册子应用,避免一开始就 load 所有资源。

const apps = [
  {
    name: 'lazy-react',
    activeRule: '/react',
    async loader(props) {
      props.loading(true);
      const app = await import('./apps/react-entry');
      props.loading(false);
      return app;
    },
  },
];

gzip 后主应用不到 100KB,子应用平均 300KB 左右,网络好的情况下 2 秒内能进页面。不算快,但比以前 iframe 卡顿强多了。

回顾与反思

现在回头看,qiankun 确实解决了多团队协作和技术栈混杂的问题,但也带来了额外复杂度。比如本地联调特别麻烦,每个人都要起一堆服务;构建流程也变长了,CI 要打多个包。

做得好的地方是隔离基本可控,通信够用,上线后没出过大事故。不足的地方是开发体验差,错误堆栈经常被沙箱搞得面目全非,debug 起来像在猜谜。

有个遗留问题到现在没彻底解决:子应用里用了 Web Components 的话,shadow DOM 嵌套容易炸,目前靠约定规避——不让在子应用里写自定义元素。这不是最优解,但影响不大,就先放着了。

以上是我的项目经验,希望对你有帮助

这个方案不是完美的,尤其是对新人不太友好,文档也得自己补。但如果你也在整合同构多系统的项目,qiankun 至少是个能跑通的选择。亲测有效,但提前准备好踩坑的心态。

fetch(‘https://jztheme.com/api/user’) 这种跨域请求记得配代理,不然开发环境会疯。其他细节欢迎评论区交流,有更优的实现方式我也想学学。

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

暂无评论