POST · 前端 · 文章
微前端学习笔记:qiankun / micro-app / 无界
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 6350 字 · 预计阅读 16 分钟
本文覆盖微前端的适用场景、核心概念,以及 qiankun、micro-app、无界(wujie)三个方案的基础使用、渲染机制、沙箱、路由与通信,最后给出横向对比与选型建议。
- 版本基线:qiankun 2.x、micro-app 1.x、无界 1.x(API 会随版本变化,落地前以官方文档为准)
- 示例栈:Vue 3 + Vite / React 18 + Vite,主应用为 history 路由
- 说明:三个方案都在快速演进,本文侧重"原理与用法共性",具体选项以对应版本文档为准
一、微前端是什么
微前端是把一个前端应用拆成多个可以独立开发、独立部署、运行时组合的子应用,再由一个主应用把它们拼在一起的架构方式。
它解决的问题:
| 问题 | 微前端的作用 |
|---|---|
| 单体应用过大,构建慢、发布互相阻塞 | 子应用独立构建、独立发布 |
| 多团队技术栈不同(Vue 2 / Vue 3 / React) | 每个子应用可用自己的框架与版本 |
| 老系统无法一次性重写 | 新功能做成子应用,逐步替换旧模块 |
| 公共模块(登录、菜单、权限)重复实现 | 主应用统一承载,子应用复用 |
代价(必须提前评估):
- 复杂度上升:多一套构建、部署、调试、监控流程。
- 隔离不完美:JS 全局变量、样式、路由都可能互相影响,沙箱只能缓解。
- 性能开销:多份运行时、重复依赖、跨应用通信成本。
- 排障变难:问题可能出在主子应用交界处,需要统一的日志与错误上报。
不该用的场景:只有一个团队、一个仓库、发布节奏一致的小中型项目——单体或普通的多包仓库(monorepo)更简单。
二、方案全景
| 方案 | 隔离方式 | 接入成本 | 特点 |
|---|---|---|---|
| iframe | 浏览器原生(完全隔离) | 低 | 隔离最彻底,但路由、弹窗、通信、体验都受限 |
| single-spa | 无沙箱,需自行处理 | 中 | 微前端底座,qiankun 基于它 |
| qiankun | JS 沙箱 + 样式隔离 | 中 | 生态成熟、社区资料多、约定较多 |
| micro-app | CustomElement + iframe 沙箱 | 低 | 使用方式接近写标签,支持 Vite |
| 无界(wujie) | iframe(JS)+ Shadow DOM(CSS) | 低 | 隔离最彻底,支持保活与路由同步 |
| Module Federation | 无(模块共享) | 中 | 构建期共享模块,不是完整微前端方案 |
Module Federation 解决的是"模块复用与联合构建",运行时的应用编排、路由、沙箱仍需自行处理,通常与上述方案组合使用。
三、通用概念
3.1 主应用与子应用
- 主应用(基座):负责布局、登录、菜单、权限、路由分发、加载子应用。
- 子应用:独立的项目与构建产物,对外暴露生命周期或被标签/组件挂载。
3.2 生命周期
几乎所有方案都要求子应用暴露三个生命周期(名称可能不同):
// 子应用入口(qiankun 约定)
export async function bootstrap() {}
export async function mount(props) {}
export async function unmount(props) {}
| 阶段 | 时机 | 典型操作 |
|---|---|---|
bootstrap |
首次加载,只执行一次 | 初始化全局配置 |
mount |
每次进入 | 挂载 DOM、启动路由 |
unmount |
每次离开 | 卸载 DOM、清理定时器与监听 |
3.3 沙箱
沙箱解决两类冲突:
| 类型 | 冲突表现 | 隔离手段 |
|---|---|---|
| JS 沙箱 | 子应用改 window,影响其它应用 |
Proxy 代理 window、快照还原、iframe 隔离 |
| 样式沙箱 | 全局 CSS 互相污染 | scoped 前缀、Shadow DOM、iframe |
3.4 路由
微前端的路由有两种模式:
- 主应用管理路由(qiankun、micro-app 的
baseroute):主应用根据 URL 决定加载哪个子应用,子应用内部再用自己的 router。 - 子应用接管路由(无界的
sync):子应用路由变化同步到主应用地址栏,刷新后可还原。
3.5 通信
| 方式 | 特点 |
|---|---|
| props 传值 | 主 → 子,简单直接 |
| 全局状态 | 主 ↔ 子,适合登录态、主题、权限 |
| 事件总线 | 任意方向,松耦合,注意解绑 |
| URL / 路由参数 | 刷新可恢复,适合页面级参数 |
四、qiankun
4.1 原理
qiankun 基于 single-spa,核心是 HTML Entry:
主应用调用 registerMicroApps
→ 根据 activeRule 匹配 URL
→ fetch 子应用 HTML(import-html-entry)
→ 解析出 script / style / template
→ 创建沙箱(Proxy window + 样式隔离)
→ 执行 script(拿到 bootstrap/mount/unmount)
→ mount 时把 template 插入容器
关键点:子应用不是 iframe,而是把 HTML 里的脚本和样式"取出来"在当前页面执行,因此需要沙箱来隔离。
4.2 主应用接入
// main.js
import { createApp } from "vue";
import { registerMicroApps, start } from "qiankun";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");
registerMicroApps([
{
name: "vue-app",
entry: "//localhost:7101",
container: "#subapp-container",
activeRule: "/vue",
props: { token: "xxx" }
},
{
name: "react-app",
entry: "//localhost:7102",
container: "#subapp-container",
activeRule: "/react"
}
]);
start({
prefetch: "all", // 预加载:all | true | false
sandbox: {
strictStyleIsolation: false, // Shadow DOM 隔离
experimentalStyleIsolation: true // scoped 前缀隔离
},
singular: true // 同一时间只渲染一个子应用
});
registerMicroApps 的第二个参数是全局生命周期:
registerMicroApps(apps, {
beforeLoad: [app => console.log("before load", app.name)],
beforeMount: [app => console.log("before mount", app.name)],
afterUnmount: [app => console.log("after unmount", app.name)]
});
手动加载(适合弹窗、局部嵌入):
import { loadMicroApp } from "qiankun";
const microApp = loadMicroApp({
name: "vue-app",
entry: "//localhost:7101",
container: "#dialog-container",
props: { mode: "dialog" }
});
// 卸载
await microApp.unmount();
4.3 子应用改造
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
let instance = null;
function render(props = {}) {
const { container } = props;
instance = createApp(App);
instance.use(router);
instance.mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount(props) {
render(props);
}
export async function unmount() {
instance.unmount();
instance = null;
}
配套修改:
- publicPath:资源必须能被主应用按绝对地址加载。
// vite.config.js
export default defineConfig({
base: "/vue/", // 与 activeRule 对应
server: { cors: true, headers: { "Access-Control-Allow-Origin": "*" } }
});
- 路由 base:子应用路由要带上前缀。
createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? "/vue" : "/"),
routes
});
CORS:主应用与子应用不同源时,子应用静态资源必须允许跨域(
Access-Control-Allow-Origin)。Vite 限制:qiankun 依赖 UMD 与
eval执行脚本,Vite 的 ESM 产物不能直接接入,需要vite-plugin-qiankun之类的插件,或改用 webpack 构建。
4.4 渲染
| 阶段 | 行为 |
|---|---|
| 加载 | 拉取 HTML,解析出 JS/CSS 与模板 |
| 执行 | 在沙箱中执行 JS,得到生命周期函数 |
| 挂载 | 把模板插入 container,调用 mount |
| 卸载 | 调用 unmount,清理容器内容与沙箱 |
注意:qiankun 不会自动卸载子应用的全局副作用(定时器、window 监听、事件总线),必须在 unmount 里手动清理,否则重复进入会重复绑定。
4.5 沙箱
| 沙箱 | 说明 | 使用 |
|---|---|---|
proxySandbox |
用 Proxy 代理 window 的读写,写操作不落到真实 window | 2.x 默认 |
legacySandbox |
记录变更,卸载时还原 | 兼容旧场景 |
snapshotSandbox |
挂载前快照、卸载后还原 | 单实例、低版本行为 |
strictStyleIsolation |
Shadow DOM 隔离样式 | 需注意组件库弹窗挂到 body |
experimentalStyleIsolation |
给子应用样式加前缀 | 兼容性更好,但 :root、body 选择器仍会穿透 |
已知限制:
document.body相关的样式(全局字体、body背景)无法隔离。- 子应用往
document.body追加的弹窗不受 Shadow DOM 约束。 - 多个子应用同时挂载时,全局样式冲突仍可能发生。
4.6 路由
// 主应用:activeRule 决定何时加载
{ name: "vue-app", activeRule: "/vue" }
- 子应用内部路由必须以
/vue为前缀,且createWebHistory('/vue')。 - 主应用跳转子应用:
router.push('/vue/detail')。 - 子应用跳主应用:通过 props 传入主应用的路由实例,或在
unmount后由主应用接管。 - 刷新问题:子应用直接访问
/vue/detail时,主应用的静态服务器需要把该路径回退到index.html(history 模式通用问题)。
4.7 通信
方式一:props
// 主应用
registerMicroApps([{ name: "vue-app", entry, activeRule, props: { token, user } }]);
// 子应用 mount
export async function mount(props) {
console.log(props.token, props.user);
}
方式二:全局状态 initGlobalState
// 主应用
import { initGlobalState, MicroAppStateActions } from "qiankun";
const actions = initGlobalState({ user: null, theme: "light" });
actions.onGlobalStateChange((state, prev) => {
console.log("主应用收到变化", state, prev);
});
actions.setGlobalState({ user: { name: "张三" } });
// 子应用 mount 中接收
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log("子应用收到", state);
}, true); // true 表示立即执行一次
props.setGlobalState({ theme: "dark" });
}
要点:
- 全局状态是单向数据流:任何一方
setGlobalState都会通知所有订阅者。 - 不要在状态里放不可序列化对象(组件实例、DOM)。
- 子应用卸载时记得取消监听(qiankun 会尝试清理,但自定义订阅需要自行处理)。
4.8 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 子应用资源 404 | publicPath 未设置,仍按相对路径加载 |
设置 base/publicPath |
| 白屏、控制台 CORS 报错 | 子应用未允许跨域 | 静态服务加 Access-Control-Allow-Origin |
| 刷新子路由 404 | history 模式下服务器未回退 | 配置 try_files 或 historyApiFallback |
| 样式串台 | 样式隔离未开或穿透到 body | 开 experimentalStyleIsolation,收敛全局样式 |
| 重复挂载/事件重复触发 | unmount 未清理定时器与监听 |
在 unmount 里统一清理 |
| Vite 子应用跑不起来 | qiankun 不支持 ESM 产物 | 用插件或改 webpack |
| 图标字体 404 | 字体路径相对,未走 publicPath | 字体改用绝对地址或打包引入 |
五、micro-app
5.1 原理
micro-app 基于 CustomElement(Web Components):
主应用写 <micro-app name url baseroute>
→ 浏览器创建自定义元素并触发 connectedCallback
→ micro-app 拉取子应用 HTML/JS/CSS
→ 在 iframe 沙箱中执行 JS(提供独立 window)
→ 把 DOM 渲染到自定义元素内部
→ 路由变化时卸载/挂载
它与 qiankun 最大的差异是:接入方式是标签,主应用不需要为每个子应用写注册配置;沙箱默认用 iframe 提供独立 window,天然隔离更彻底。
5.2 主应用接入
npm i @micro-zoe/micro-app
// main.js
import microApp from "@micro-zoe/micro-app";
microApp.start({
// 可选:预加载、插件、全局生命周期
// preFetchApps: [{ name: "vue-app", url: "//localhost:7101/" }]
});
<!-- 页面中直接使用 -->
<micro-app
name="vue-app"
url="//localhost:7101/"
baseroute="/vue"
:data="dataForChild"
@datachange="handleDataChange"
></micro-app>
常用属性:
| 属性 | 作用 |
|---|---|
name |
子应用唯一标识,通信时使用 |
url |
子应用地址(含协议与端口) |
baseroute |
子应用路由前缀,配合子应用 router base |
data |
主应用下发的数据(对象) |
disableSandbox |
关闭沙箱 |
shadowDOM |
开启 Shadow DOM 样式隔离 |
clear-data |
卸载时清空通信缓存 |
keep-alive |
保活(保留 DOM 与状态) |
5.3 子应用改造
micro-app 的要求比 qiankun 少,通常只需要:
- 处理路由 base:
const baseRoute = window.__MICRO_APP_BASE_ROUTE__ || "/";
createRouter({
history: createWebHistory(baseRoute),
routes
});
处理 publicPath(Vite):
base设置为子应用部署路径。静态资源跨域:与 qiankun 相同。
生命周期(可选):不需要导出
bootstrap/mount/unmount,micro-app 会自动处理挂载;需要自定义时用window.microApp或元素事件。
5.4 渲染
| 阶段 | 行为 |
|---|---|
| 连接 | <micro-app> 进入文档,触发元素生命周期 |
| 加载 | 拉取 HTML、解析资源、创建沙箱 |
| 渲染 | 在自定义元素内部生成 DOM,执行子应用脚本 |
| 卸载 | 移除 DOM,销毁沙箱 |
micro-app 的 DOM 直接在自定义元素内,没有 iframe 的 DOM 层级问题,弹窗、document.body 追加等行为比纯 iframe 方案自然。
5.5 沙箱
- 默认:iframe 沙箱 + Proxy window。子应用的 JS 运行在 iframe 提供的独立环境中,通过 Proxy 映射到主应用的 DOM,从而既隔离 JS 又能正常渲染。
disableSandbox:关闭后子应用与主应用共享 window,性能略好但隔离消失(此时通信需要EventCenterForMicroApp)。- 样式隔离:默认 scoped(给样式加前缀),可选
shadowDOM(更彻底,但组件库弹窗可能挂到 body 导致样式丢失)。
5.6 路由
<micro-app name="vue-app" url="//localhost:7101/" baseroute="/vue"></micro-app>
- 主应用路由
/vue/*时,baseroute告诉子应用自己的前缀,子应用用window.__MICRO_APP_BASE_ROUTE__初始化 router。 - 主应用切换路由离开
/vue时,micro-app 自动卸载子应用。 - 子应用内部跳转由子应用自己的 router 完成,地址栏仍是主应用地址(history 模式下需要主应用路由支持该前缀)。
5.7 通信
主 → 子
import microApp from "@micro-zoe/micro-app";
// 主动下发(name 与 <micro-app> 的 name 一致)
microApp.setData("vue-app", { user: { name: "张三" } });
// 取子应用数据
const data = microApp.getData("vue-app");
子 → 主
// 子应用
window.microApp.dispatch({ type: "child-ready" });
// 主应用监听
microApp.addDataListener("vue-app", data => {
console.log("来自子应用", data);
});
子应用接收主应用数据
const data = window.microApp.getData();
function listener(payload) {
console.log("主应用下发", payload);
}
window.microApp.addDataListener(listener, true); // true:有缓存数据时立即触发
全局数据(跨应用)
microApp.setGlobalData({ theme: "dark" }); // 主应用
window.microApp.getGlobalData(); // 任意应用读取
要点:
setData/dispatch的数据会缓存,卸载后仍保留,必要时用clear-data或clearData清理。dispatch只接受对象,且会做浅合并与去重(第一层 key 变化才发送)。- 数据是异步合并发送的,连续调用会在下一帧合并。
5.8 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 子应用路由错乱 | 未使用 baseroute / 未取 __MICRO_APP_BASE_ROUTE__ |
统一 base 配置 |
| 样式丢失 | 开了 shadowDOM,组件库弹窗挂到 body |
关掉 shadowDOM,或把弹窗容器挂到应用内 |
| 子应用 window 上挂载的库找不到 | 沙箱隔离后 window 不是全局 | 改用模块导入,或用 disableSandbox |
| 通信数据不更新 | 只改了嵌套对象内部字段(浅比较) | 传新对象,或用 forceSetData |
| 卸载后数据还在 | 通信数据有缓存 | 加 clear-data |
六、无界(wujie)
6.1 原理
无界用 iframe + Shadow DOM 组合,把"JS 运行环境"和"渲染位置"分开:
主应用渲染 <wujie-vue name url>
→ 创建同域 iframe,加载子应用 HTML 与 JS(独立 window/document/路由)
→ 子应用在 iframe 中执行,操作 document 时被 Proxy 拦截
→ 拦截到的 DOM 操作落到主应用的 Shadow DOM 中(用户看到的是主应用页面的一部分)
因此:
- JS 隔离由 iframe 天然提供(独立 window、独立全局变量、独立路由)。
- 样式隔离由 Shadow DOM 提供。
- 子应用代码几乎不需要改造(不需要导出生命周期,也不需要改 publicPath)。
6.2 主应用接入
npm i wujie-vue3
// main.js
import { createApp } from "vue";
import WujieVue from "wujie-vue3";
import App from "./App.vue";
createApp(App).use(WujieVue).mount("#app");
<template>
<wujie-vue
name="vue-app"
:url="'//localhost:7101/'"
:alive="true"
:sync="true"
:props="{ token }"
:before-load="onBeforeLoad"
:after-mount="onAfterMount"
/>
</template>
常用属性:
| 属性 | 作用 |
|---|---|
name |
唯一标识,缓存与通信使用 |
url |
子应用地址 |
alive |
保活:切换走时不销毁,状态保留 |
sync |
子应用路由同步到主应用地址栏 |
props |
传给子应用的数据(window.$wujie.props) |
fetch |
自定义资源请求(可加 token、改请求头) |
plugins |
插件(如给子应用注入全局变量) |
fiber |
开启 fiber 模式(优化渲染性能,注意兼容性) |
degrade |
降级为纯 iframe 模式 |
生命周期:
<wujie-vue
name="vue-app"
:url="url"
:before-load="() => console.log('beforeLoad')"
:before-mount="() => console.log('beforeMount')"
:after-mount="() => console.log('afterMount')"
:before-unmount="() => console.log('beforeUnmount')"
:after-unmount="() => console.log('afterUnmount')"
:activated="() => console.log('activated')"
:deactivated="() => console.log('deactivated')"
/>
6.3 渲染
| 阶段 | 行为 |
|---|---|
| 加载 | 在 iframe 中加载子应用 HTML,执行脚本 |
| 渲染 | 子应用的 DOM 通过代理渲染到 Shadow DOM |
| 保活 | alive 为 true 时,离开不销毁,仅隐藏 |
| 卸载 | 销毁 iframe 与 Shadow DOM |
注意:保活(alive)会保留子应用状态,意味着子应用不会重新执行 mounted;需要在 activated 里刷新数据。
6.4 沙箱
| 隔离 | 手段 | 说明 |
|---|---|---|
| JS | iframe | 独立 window、全局变量、事件循环 |
| CSS | Shadow DOM | 样式作用域天然隔离 |
| 路由 | iframe 内 history + sync |
子应用路由独立,可同步到地址栏 |
优点:隔离最彻底,老项目几乎零改造。 限制:
- iframe 内的弹窗、
document.body、焦点管理需要无界的代理补齐,边界场景仍有差异。 - iframe 会增加内存占用,多应用常驻时更明显。
sync需要主子应用的路由配合,hash 模式下 query 处理有额外细节。
6.5 路由
sync 为 true 时,子应用路由变化会同步到主应用地址栏:
<wujie-vue name="vue-app" url="//localhost:7101/" :sync="true" />
跳转方式:
- 主应用跳子应用:主应用
router.push('/vue/detail'),子应用根据 URL 同步。 - 子应用跳主应用:主应用通过
props传入jump函数,子应用调用。
// 主应用
const jump = location => router.push(location);
// 模板
<wujie-vue name="vue-app" :url="url" :props="{ jump }" />
// 子应用
window.$wujie?.props.jump({ path: "/order" });
已保活的应用再跳指定路由,需要通信配合:
// 子应用 A
window.$wujie?.bus.$emit("routeChange", "/test");
// 子应用 B
window.$wujie?.bus.$on("routeChange", path => router.push(path));
6.6 通信
| 方式 | 方向 | 用法 |
|---|---|---|
props |
主 → 子 | 子应用读 window.$wujie.props |
bus |
任意 | window.$wujie.bus.$emit/$on/$off |
sync + URL |
双向 | 路由参数同步 |
window.$wujie 其他能力 |
子 → 主 | 如 $wujie.props.jump、$wujie.location |
// 主应用下发
<wujie-vue name="vue-app" :url="url" :props="{ user, theme }" />
// 子应用读取
const { user, theme } = window.$wujie.props;
// 事件总线(推荐用于跨应用通知)
window.$wujie.bus.$emit("user-updated", user);
window.$wujie.bus.$on("user-updated", user => {});
window.$wujie.bus.$off("user-updated", handler);
注意:bus 是全局的,多个子应用共享,事件名要加前缀(order:refresh),并在 unmount/deactivated 时解绑。
6.7 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 子应用弹窗位置错乱 | 弹窗挂到 iframe 的 body | 使用无界的弹窗兼容方案,或把弹窗改为应用内渲染 |
| 刷新后路由丢失 | sync 未开启或路由未还原 |
开 sync,确认子应用路由模式一致 |
| 保活后数据不刷新 | alive 保留了旧状态 |
在 activated 中主动刷新 |
| 内存上涨 | 多个 iframe 常驻 | 减少 alive 应用数量,或及时销毁 |
| 静态资源 404 | 子应用 publicPath 不对 |
配置 base/publicPath 为部署绝对路径 |
七、三者对比
7.1 基础能力
| 维度 | qiankun | micro-app | 无界(wujie) |
|---|---|---|---|
| 底层方案 | single-spa + HTML Entry | CustomElement + iframe 沙箱 | iframe + Shadow DOM |
| 主应用接入 | 注册配置(registerMicroApps) |
写标签(<micro-app>) |
写组件(<wujie-vue>) |
| 子应用改造量 | 较大(生命周期、publicPath、router base) | 较小(router base、publicPath) | 几乎为零 |
| JS 隔离 | Proxy 沙箱(默认) | iframe + Proxy | iframe(原生隔离) |
| 样式隔离 | scoped / Shadow DOM(可选) | scoped / Shadow DOM(可选) | Shadow DOM(默认) |
| 路由接管 | 主应用主导(activeRule) |
主应用主导(baseroute) |
子应用主导(sync) |
| 保活 | 需自行实现(loadMicroApp + 缓存 DOM) |
keep-alive |
alive |
| Vite 支持 | 需插件 | 原生支持 | 原生支持 |
| 预加载 | 内置 prefetch |
preFetchApps |
需自行实现 |
7.2 通信能力
| 能力 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 主 → 子 | props / setGlobalState |
data 属性 / setData |
props |
| 子 → 主 | setGlobalState |
dispatch / datachange |
bus / props 回调 |
| 跨应用广播 | 需自行封装 | setGlobalData |
bus(全局) |
| 数据持久化 | 无(状态在内存) | 有缓存(需 clear-data 清理) |
随 alive 保留 |
| 使用复杂度 | 中 | 低 | 低 |
7.3 优势与不足
qiankun
- 优势:生态最成熟、文档与案例多;
loadMicroApp支持手动挂载;全局状态 API 清晰;社区插件丰富。 - 不足:子应用改造量最大(生命周期、publicPath、路由 base、跨域都要处理);Vite 不原生支持;沙箱是"尽力隔离",全局样式与 body 级副作用仍会穿透;保活需要自己实现。
micro-app
- 优势:接入方式最像写标签,改造成本低;默认 iframe 沙箱,隔离较强;原生支持 Vite;通信 API 完整(含全局数据与缓存机制);支持 Shadow DOM。
- 不足:通信数据有缓存语义,需要理解合并与去重规则;
shadowDOM与组件库弹窗存在兼容问题;生态与社区资料少于 qiankun。
无界(wujie)
- 优势:iframe + Shadow DOM,隔离最彻底、老项目几乎零改造;
alive保活与sync路由同步开箱即用;支持 Vite;插件机制可注入依赖。 - 不足:iframe 带来内存与边界问题(弹窗、焦点、同域要求);多应用常驻时资源占用偏高;路由同步在 hash 模式下有额外处理;社区规模小于 qiankun。
7.4 场景对照
| 场景 | 推荐 |
|---|---|
| 新项目、团队已熟悉 qiankun、子应用可改造 | qiankun |
| 老项目(jQuery/Vue2)改造预算有限,要求隔离 | 无界 |
| 子应用多、技术栈杂、想少改代码,需要 Vite | micro-app |
| 只需要共享模块、不要求独立部署 | Module Federation |
| 简单嵌入第三方页面、无通信需求 | iframe |
实际落地常见组合:
- qiankun + webpack:最稳的组合,资料最多。
- micro-app / 无界 + Vite:新项目友好,接入快。
- 主应用统一登录 + 权限,子应用只关心业务:三个方案都支持,关键是把鉴权放在主应用并通过 props/状态下发。
八、选型建议
决策顺序建议:
- 先问是否真的需要微前端:一个团队、发布节奏一致时,monorepo 或单体更划算。
- 看子应用能否改造:不能改(老系统、第三方)→ 无界;能改但不想大改 → micro-app;能接受改造 → qiankun。
- 看构建工具:Vite 为主 → micro-app / 无界;webpack 为主 → qiankun 也可。
- 看隔离要求:要求强隔离(多套组件库、样式冲突严重)→ 无界、micro-app(iframe 沙箱)。
- 看团队维护能力:需要长期自研插件、深度定制 → qiankun 社区资源多;希望少维护 → micro-app/无界开箱即用。
九、共性工程问题
9.1 公共依赖
多个子应用重复打包 Vue/React 会增大体积。做法:
- 主应用通过
externals或import maps提供公共库,子应用不打包。 - 或接受重复(隔离优先),用 HTTP 缓存抵消。
注意:共享同一份运行时(同一 Vue 实例)会带来状态与版本耦合,隔离优先时不要强行共享。
9.2 部署
| 项 | 建议 |
|---|---|
| 域名 | 主子应用同域(/vue、/react)可减少 CORS 与 cookie 问题 |
| 静态资源 | 子应用使用绝对地址(base/publicPath),不要依赖相对路径 |
| 缓存 | 子应用入口 HTML 用 no-cache,带 hash 的资源长缓存 |
| 灰度 | 子应用按版本部署,主应用按用户/环境决定加载哪个地址 |
9.3 版本与发布
- 主应用记录子应用版本(如
manifest.json),避免"主应用改了但子应用没同步"。 - 子应用接口变更要向后兼容,或主应用按版本下发配置。
- 发布顺序:先发子应用(向后兼容),再发主应用。
9.4 监控与降级
- 统一错误上报:主子应用使用同一套 SDK,带上
appName标记。 - 子应用加载失败要有兜底 UI 与重试,不要让整个页面白屏。
- 关键子应用准备降级页(静态提示 + 跳转)。
9.5 样式冲突排查步骤
- 确认隔离方式(scoped / Shadow DOM / 无)。
- 用 DevTools 看冲突元素的计算样式来源。
- 检查是否有全局选择器(
*、body、:root、组件库 reset)。 - 临时开启强隔离验证;确认后再决定是收敛全局样式还是换隔离方案。
十、迁移路径
从单体到微前端,推荐渐进式:
- 拆公共层:登录、权限、布局、菜单先抽到主应用。
- 选一个低风险模块做第一个子应用(如报表、帮助中心)。
- 打通链路:路由、通信、部署、监控。
- 逐步迁移:每次迁一个模块,旧模块保留在主应用,直到清空。
- 收尾:统一构建规范、依赖版本、错误上报。
从 iframe 迁到框架方案:
- 先保留 iframe 作为降级路径,新模块用框架接入。
- 逐步替换 iframe 中的老页面,注意 cookie、跨域、弹窗兼容。
十一、FAQ
Q1:微前端能解决样式冲突吗?
只能缓解。scoped 与 Shadow DOM 都能隔离大部分样式,但 body、:root、全局 reset、挂到 body 的弹窗仍会穿透。彻底隔离要用 iframe(无界、micro-app 的默认沙箱)。
Q2:qiankun 为什么要求子应用改 publicPath?
因为子应用的资源是从主应用页面加载的,相对路径会以主应用地址为基准,必须改成绝对路径或加前缀。
Q3:Vite 项目能接 qiankun 吗?
不原生支持(qiankun 依赖 UMD 与 eval 执行),需要 vite-plugin-qiankun 等社区方案,或用 webpack 构建子应用。
Q4:micro-app 和 qiankun 最大的区别?
接入方式与沙箱:micro-app 用标签 + iframe 沙箱,改造成本低;qiankun 用注册配置 + Proxy 沙箱,生态更成熟。
Q5:无界的 iframe 会不会很慢?
首次加载有 iframe 初始化成本,但可以预加载、保活;多应用常驻时内存占用明显,需要控制 alive 数量。
Q6:子应用之间能直接通信吗?
qiankun 需借助主应用的全局状态;micro-app 用 setGlobalData;无界用全局 bus。都建议通过主应用中转,避免子应用互相耦合。
Q7:刷新页面后子应用路由丢失怎么办?
qiankun 靠主应用路由 + 服务器回退;micro-app 靠 baseroute + __MICRO_APP_BASE_ROUTE__;无界开 sync 同步路由到地址栏。
Q8:需要保活吗?
看场景:频繁切换的列表页建议保活;表单页保活后要处理"数据残留"(在 activated 里重置)。保活会占内存,不宜过多。
Q9:子应用能独立运行吗?
应该可以。三个方案都要求子应用"单独启动也能跑"(qiankun 用 __POWERED_BY_QIANKUN__ 判断),否则调试成本很高。
Q10:微前端和 iframe 的本质区别?
iframe 是完全独立的文档,隔离彻底但通信与体验受限;微前端方案让子应用与主应用共享同一个文档(或部分 DOM),体验更自然,但需要沙箱来弥补隔离。