从落地到"半停用":qiankun 微前端在一个 Vue 3 + Vite 项目中的完整实践与反思

本文基于一个真实企业项目——某工业监测管理平台(已对业务信息做脱敏处理)的微前端改造实践整理而成。系统由管理后台(admin)与大屏展示端(client)两个独立 Vue 3 应用组成,曾用 qiankun 集成,后转为独立部署。这篇文章既讲"怎么接的",也讲"为什么后来不用了",希望能给正在选型微前端的团队一些参考。

一、项目背景

系统包含两个独立前端应用:

应用 定位 技术栈
admin 管理后台主应用,基于 JeecgBoot Vue3 二次开发 Vue 3.4 + TypeScript + Vite 5 + Ant Design Vue 4 + Pinia
client 大屏展示应用 Vue 3.2 + JavaScript + Vite 4 + Three.js / 地图 SDK / ECharts

两个应用由不同时期、不同风格的团队开发:admin 是标准的中后台技术栈,client 则是重渲染、重 WebSocket、全屏运行的大屏应用。

最初的诉求很典型:

  1. 用户在 admin 里点一个菜单,希望能无刷新进入大屏,而不是跳到一个新站点;
  2. 登录态(token)要共享,不能让用户再登一次;
  3. 两个应用保持独立仓库目录、独立构建部署节奏。

于是自然想到了 qiankun——国内最主流的微前端框架,基于 single-spa,通过 HTML Entry + JS 沙箱实现子应用接入,对技术栈几乎无侵入。

二、主应用侧的实现

2.1 环境变量驱动的微应用注册清单

qiankun 的第一步是 registerMicroApps。我们没有把子应用列表硬编码,而是约定了一个环境变量前缀 VITE_APP_SUB_,启动时动态扫描生成注册清单:

// admin/src/qiankun/apps.ts
const _apps: object[] = [];
for (const key in import.meta.env) {
  if (key.includes('VITE_APP_SUB_')) {
    const name = key.split('VITE_APP_SUB_')[1];
    const obj = {
      name,                                  // 微应用名称,全局唯一
      entry: import.meta.env[key],           // 微应用入口地址
      container: '#content',                 // 挂载节点
      activeRule: name,                      // 激活路由前缀
    };
    _apps.push(obj);
  }
}
export const apps = _apps;

对应的 .env.development

# 命名必须以 VITE_APP_SUB_ 开头,client 为子应用项目名称,也是路由父路径
VITE_APP_SUB_client = '//localhost:3010'

这样做的好处:

  • 新增子应用零代码改动——加一行环境变量即可;
  • 环境隔离天然成立——开发指向 //localhost:3010,生产指向部署域名;
  • name 同时充当 activeRule,约定"子应用名即路由前缀",即访问 /client/** 时激活 client 子应用。

2.2 注册与启动

// admin/src/qiankun/index.ts(节选)
import { registerMicroApps, start, runAfterFirstMounted, addGlobalUncaughtErrorHandler } from 'qiankun';
import { apps } from './apps';
import { getProps, initGlState } from './state';

function genActiveRule(routerPrefix) {
  return (location) => location.pathname.startsWith(routerPrefix);
}

function filterApps() {
  apps.forEach((item) => {
    item.props = getProps();                 // 主应用下发给子应用的数据
    item.activeRule = genActiveRule('/' + item.activeRule);
  });
  return apps;
}

function registerApps() {
  const _apps = filterApps();
  registerMicroApps(_apps, {
    beforeLoad: [(loadApp) => console.log('before load', loadApp)],
    beforeMount: [(mountApp) => console.log('before mount', mountApp)],
    afterMount: [(mountApp) => console.log('after mount', mountApp)],
    afterUnmount: [(unloadApp) => console.log('after unload', unloadApp)],
  });
  runAfterFirstMounted(() => console.log('开启监控'));
  addGlobalUncaughtErrorHandler((event) => console.log(event));
  initGlState();
  start({});
}

export default registerApps;

几个细节值得展开:

activeRule 用函数而非字符串。 genActiveRule 返回一个 location => boolean 的函数,比字符串匹配灵活——后续如果要支持 /client/client/xxx 之外的复杂规则(比如 hash 模式、多前缀),改函数即可。

生命周期钩子是埋点/监控的天然切面。 beforeLoad(资源加载前)、beforeMount/afterMount(挂载前后)、afterUnmount(卸载后)可以用来做加载耗时上报、子应用切换埋点。runAfterFirstMounted 则专门用于首个子应用挂载后开启监控脚本——避免监控脚本在子应用加载前就跑起来,统计到一堆空白时间。

addGlobalUncaughtErrorHandler 兜底。 微前端场景下,子应用的未捕获异常会冒泡到主应用,统一在这里上报,避免大屏里 Three.js 的渲染异常把整个后台搞崩而毫无感知。

2.3 数据通信:props 下发 + 全局状态两条通道

qiankun 的主子通信我们用了两条通道,分别解决不同的问题。

通道一:props 直传——解决"登录态与上下文共享"

// admin/src/qiankun/state.ts(节选)
import { initGlobalState } from 'qiankun';
import { store } from '/@/store';
import { router } from '/@/router';
import { getToken } from '/@/utils/auth';

export function getProps() {
  return {
    data: {
      publicPath: '/',
      token: getToken(),   // 登录态
      store,               // 主应用 Pinia 实例
      router,              // 主应用路由实例
    },
  };
}

子应用在 mount(props) 生命周期里直接拿到 token,无需再走一遍 SSO/登录流程;拿到 store 和 router 实例,则可以做一些深度联动(比如大屏里点击设备跳回后台对应详情页)。

注意:直接传 store/router 实例是"强耦合"方案,要求子应用与主应用的 Pinia / Vue Router 版本兼容。这在"同一团队维护的两个应用"里可接受,但如果你追求子应用完全技术栈无关,应该只传纯数据。

通道二:initGlobalState——解决"双向响应式通信"

export function initGlState(info = { userName: 'admin' }) {
  const actions = initGlobalState(info);
  actions.setGlobalState(info);
  actions.onGlobalStateChange((newState, prev) => {
    console.info('newState', newState);
    console.info('prev', prev);
  });
  return actions;
}

qiankun 内置的 GlobalState 是一个观察者模式的全局状态池:主应用 setGlobalState,子应用通过 props 里的 onGlobalStateChange 监听;反过来子应用也可以 setGlobalState 通知主应用。适合做主题切换、用户信息变更这类低频、双向的信号同步。

2.4 挂载容器:藏在布局组件里的 #content

子应用挂载点 #content 放在主应用布局的内容区:



同时配合 JeecgBoot 的动态路由机制,在后端菜单里把某个菜单项的 component 配置为 LayoutsContent

// admin/src/router/helper/routeHelper.ts
LayoutMap.set('LAYOUT', LAYOUT);
LayoutMap.set('IFRAME', IFRAME);
// 微前端 qiankun
LayoutMap.set('LayoutsContent', LayoutContent);

这样"进入大屏"就变成了一个正常的菜单路由,点击菜单 → URL 变为 /client/... → qiankun 的 activeRule 命中 → 子应用挂载到 #content。整个体验是"后台里的一个页面",而不是"跳去了另一个网站"。

2.5 防重复启动:window.qiankunStarted

启动代码放在布局组件的 onMounted 里,而布局组件可能因路由/权限刷新被多次挂载,start() 重复调用会报错。用一个全局标志位防重:

onMounted(() => {
  if (openQianKun == 'true') {
    if (!window.qiankunStarted) {
      window.qiankunStarted = true;
      registerApps();
    }
  }
});

这是 qiankun + Vue 项目的一个经典坑:注册和启动必须且只能执行一次。更优雅的做法是放到 main.ts 的初始化阶段,但放在布局 onMounted 里可以确保挂载容器已存在,各有取舍。

三、子应用侧:Vite 是最大的坑

3.1 为什么 Vite 项目接 qiankun 特别麻烦

qiankun(底层 import-html-entry)的工作方式是:抓取子应用 entry 的 HTML → 解析出内联/外链的 script 和 style → 用 eval(在沙箱上下文中)执行脚本,从而拿到子应用导出的 bootstrap / mount / unmount 生命周期。

问题来了:Vite 开发模式的产物是原生 ESM


原文地址: https://www.cveoy.top/t/topic/qHyg 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录