别急着上微前端:从 6 个场景倒推 Module Federation 2.0 与 qiankun 2.x 的选型
一句话结论:微前端买的是「独立部署」,不是代码复用。
一、从一个卡住的发布日说起
周四下午三点,本周唯一的发布窗口。订单组只改了购物车的一行文案,财务组等着这个窗口发报表修复,客服组因为全量回归没跑完不敢合并。三个团队、一个仓库,谁改代码都得等所有人一起上线。
于是有人提:「上微前端吧。」先别急着挑框架。这里被卡住的是发布流水线 ,不是代码结构。微前端这套东西从 micro-frontends.org 的定义开始,说的就是「让多个团队能独立交付特性」的技术、策略与方法------关键词是多个团队,qiankun 官方文档也直接引用了这个定义。
结论先放三句,后面所有内容都是展开它们:
- 独立部署是唯一的硬收益,其余五条常见优点都是它的派生;
- Module Federation 在模块级组合,qiankun 在应用级组合,这个粒度差异决定后面所有取舍;
- 如果现在没有两个以上团队被同一条发布流水线互相卡住,先别上。收益是零,成本却是实打实的:多套构建、运行时组合、跨应用排障与版本契约。
一句判据:能不能指出两个「上个月因为对方没发版而推迟」的需求?指不出来,就还没到该动架构的时候。
顺手排除三个常见误解:把公共组件抽成 npm 包,那是构建时组合;给页面套一层 iframe,qiankun 官方专门写过 Why Not Iframe;「前端版微服务」也不成立,后端是不是微服务并不构成前置条件,对着一个单体 API 照样能跑微前端。
二、先定边界:你到底在哪一层做组合
先问「组合发生在哪」,再问「用哪个库」。很多选型争论其实是没对齐这一层。
| 组合位置 | 发生在 | 代表做法 | 独立部署 |
|---|---|---|---|
| 构建时 | CI / 打包 | monorepo 内部包、npm 私服 | 否,随主应用一起发版 |
| 浏览器运行时 | 页面加载后 | Module Federation、qiankun、single-spa | 是,各应用各自发版 |
| 服务端 | 响应拼装 | 边缘包含、模板组合、SSR 片段 | 是,但依赖服务端路由 |
| 浏览器硬隔离 | iframe 边界 | 原生 iframe、Web Components | 是,隔离最强、体验最割裂 |
本文聚焦第二行:用户最常拿来对比、也最容易混为一谈的 Module Federation 与 qiankun。
三、两条路线的分野:模块级与应用级
一句话区分:MF 是「把别人仓库里的模块 import 进来」,qiankun 是「在页面里隔出一个带围墙的房间」。
| 维度 | Module Federation 2.0 | qiankun 2.x(生产稳定线) |
|---|---|---|
| 组合粒度 | 组件、模块、路由页 | 整个子应用 |
| 入口形态 | 远端清单 mf-manifest.json 或 remoteEntry.js |
一个 HTML 地址(HTML entry) |
| 如何接入 | import('shop/Cart'),像引本地模块 |
注册路由规则,框架注入 HTML 与生命周期 |
| 依赖处理 | 主动共享,singleton 保证只加载一份 React |
不共享运行时,各子应用自带依赖 |
| 隔离能力 | 无 JS 沙箱,共用一个全局环境 | Proxy 沙箱 + 样式隔离 |
| 技术栈自由度 | 形式上自由,实际要求 shared 版本收敛 | 高,React / Vue / Angular / jQuery 可同页 |
| 主要成本 | 共享依赖版本治理、远端不可达的降级 | 全局副作用、样式污染、路由与鉴权传递 |
最容易选错的一处 :「技术栈无关」在两条路线里含义完全不同。qiankun 允许同一页面跑两个 React 大版本;MF 的 shared 恰恰要求大家协商出同一个版本。如果真实需求是「老 AngularJS 与新 Vue 并存」,别拿 MF 当隔离方案。
所以选型的第一性问题不是「哪个更火」,而是:要拆的是模块,还是整个应用?
四、Module Federation 2.0:把跨仓库复用推迟到运行时
MF 的原始定义是「多个 JavaScript 应用之间共享代码与资源」。webpack 5 内置的是基础能力,而 Module Federation 2.0 是 module-federation/core 这套独立实现,在此之上加了 Federation Runtime、Runtime Plugins、Manifest、动态类型提示与 Chrome Devtool。
4.1 三个角色,四步运行时
- Host(消费者):页面主体,声明要用哪些远端。
- Remote(生产者) :独立构建、独立部署,用
exposes导出模块。 - Shared(共享依赖):双方约定复用的库,例如 React、设计系统。
运行时四步:拉取远端清单 → 建立 share scope 并协商版本 → 按需下载远端 chunk → 执行模块并接入模块图。
关键在于「推迟」:构建期只约定名字与共享依赖,版本协商与加载全部留到运行时,远端重新部署后 host 无需重新构建 。这就是独立部署能落地的技术原因。2.0 推荐用 mf-manifest.json 而不是 remoteEntry.js,前者额外提供动态类型提示、资源预加载与 Devtool 调试能力。
4.2 最小可跑配置(Webpack 5 / Rspack)
版本口径:@module-federation/enhanced 2.9.3(2026-11-03)、webpack 5.112.2、React 19。这个包在 exports 中提供 .、./webpack、./rspack、./runtime 四个入口,Rspack 只需把 ./webpack 换成 ./rspack,其余一致。
远端 webpack.config.js:
js
// remote / webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shop',
// 2.0 会同时生成 mf-manifest.json,供 host 使用
filename: 'remoteEntry.js',
exposes: {
'./Cart': './src/Cart.tsx',
'./ProductCard': './src/ProductCard.tsx',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
宿主侧配置与消费:
jsx
// host / webpack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'portal',
remotes: {
// 值 = 远端名@入口地址;键 shop 是 import 时用的别名
shop: 'shop@https://shop.example.com/mf-manifest.json',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
// host / src/routes.tsx ------ 远端模块像普通模块一样被 import
import { lazy, Suspense } from 'react';
const Cart = lazy(() => import('shop/Cart'));
export function CartRoute() {
return (
<Suspense fallback={<div>购物车加载中</div>}>
<Cart /> {/* 远端不可达时这里需要 ErrorBoundary 兜底 */}
</Suspense>
);
}
4.3 Vite 8 项目要多做两件事
@module-federation/vite 1.23.11 用 federation() 插件。Vite 的 remote 是原生 ESM,host 的 remotes 必须显式写 type: 'module',省略时插件会退回旧的 var 格式并给出警告。
ts
// vite.config.ts(远端)
import { defineConfig } from 'vite';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
federation({
name: 'shop',
filename: 'remoteEntry.js',
exposes: { './Cart': './src/Cart.vue' },
shared: ['vue'],
}),
],
// 远端必须声明 origin,否则 host 解析不到 chunk
server: { origin: 'http://localhost:3001' },
});
// host 侧 remotes 示例:
// shop: { type: 'module', name: 'shop',
// entry: 'http://localhost:3001/remoteEntry.js',
// entryGlobalName: 'shop', shareScope: 'default' }
4.4 三个高频坑,以及怎么验自己没踩
| 现象 | 原因 | 验收动作 |
|---|---|---|
| 页面偶发双 React、Hook 报错 | singleton: true 只保证同一 share scope 里加载一份,不替代版本纪律;requiredVersion 不满足时官方行为是「警告 + 使用现有最小版本」 |
用 2.0 的 Chrome 扩展 Shared 面板看实际生效版本与依赖图 |
| 远端一挂整块区域白屏 | manifest 404 时远端不是「慢」,而是不可用;MF 也不提供 JS 沙箱,远端与 host 共用 window |
断掉远端 CDN 演练一次,页面应显示降级 UI 而非白屏 |
| dev 正常、生产构建样式丢失 | Vite 插件的生产构建 CSS 处理是高频问题;codeSplitting 关掉会破坏 bootstrap 顺序 |
必须跑 production build 验收 CSS 与 chunk 路径,不能只看 dev server |
再补两条容易忽略的:eager: true 会把共享依赖打进入口,官方明确标注 rarely recommended;远端必须配 CORS 与稳定的 CDN 路径,host 侧要写 ErrorBoundary 与超时降级。
4.5 2.0 稳定版到底多了什么
官方博客标注的稳定日期是 2026-02-10 (npm 上 2.0.0 为 02-06)。这一版无破坏性变更,library.type 默认值由 var 改为 global,runtimePlugins 开始支持传参。对选型真正有用的增量是三项:
- 共享依赖 Tree Shaking :
runtime-infer开箱即用,server-calc由服务端汇总全局用量。官方以 Ant Design 为例,只用 Badge / Button / List 时共享包从 1404.2 KB 降到 344.0 KB,减少约 75.5%。 - SSR 与同构数据预取 :在 Modern.js 中可用,提供统一的
prefetch/cacheAPI,补的是首屏与 SEO 那块短板。 - 可观测性:manifest 生成与 AsyncStartUp 用 Rust 重写;Side Effect Scanner 静态扫描产物里的全局变量、事件监听与 CSS 影响范围;Chrome 扩展可直接看到 React 是否共享成功。
生态上 2.0 已经解绑打包器:Webpack / Rspack / Rollup / Rolldown,Rsbuild / Vite / Metro,Modern.js / Next.js / Rspress / Rslib / Storybook 都在官方列表里,也支持 Node.js 侧的 SSR、BFF。
五、qiankun 2.x:把整个应用装进带围墙的房间
qiankun 基于 single-spa,把「多应用同页」收敛成几个 API。三个关键词:HTML entry、生命周期、隔离。
5.1 原理:URL 就是集成契约
主应用只给一个 URL,不维护资源清单:qiankun 抓取子应用 HTML,解析其中的 script / style,注入容器节点。子应用导出 bootstrap、mount、unmount 三个函数,路由切换时由框架调用。这个契约简单到 jQuery 页面也能接入------把三个函数挂到 window 上就行。
隔离靠 Proxy 沙箱:把子应用对 window 的写入、定时器与事件监听圈在自己的代理对象里,卸载时统一回收。所以它比 MF 重,但能容纳完全异构的老系统。
5.2 最小可跑配置(2.10.16)
版本口径:npm i qiankun 当前装到的是 2.10.16(2023-11-15) ;3.0 仍是 RC,需要显式 npm i qiankun@rc。下面按 2.x 稳定线写,2.x 文档在 v2.qiankun.umijs.org。
主应用 main.js:
js
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{ name: 'shop', entry: '//localhost:3001',
container: '#subapp', activeRule: '/shop' },
{ name: 'finance', entry: '//localhost:3002',
container: '#subapp', activeRule: '/finance' },
]);
start({ prefetch: 'all' }); // 2.x:空闲时预取子应用资源
子应用 public-path.js 与入口:
js
// src/public-path.js ------ 必须在入口最顶部引入
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// src/main.jsx(React 18,Vue 同理)
import './public-path';
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
let root = null;
function render(props) {
const box = props.container
? props.container.querySelector('#root')
: document.querySelector('#root');
root = createRoot(box);
// 作为子应用时路由 base 必须与 activeRule 一致
root.render(<App basename={window.__POWERED_BY_QIANKUN__ ? '/shop' : '/'} />);
}
if (!window.__POWERED_BY_QIANKUN__) render({}); // 独立运行
export async function bootstrap() {}
export async function mount(props) { render(props); }
export async function unmount() { root.unmount(); root = null; }
子应用 webpack 输出配置:
js
const name = require('./package.json').name;
module.exports = {
output: {
library: name + '-[name]',
libraryTarget: 'umd',
// webpack 5 用 chunkLoadingGlobal;webpack 4 才是 jsonpFunction
chunkLoadingGlobal: 'webpackJsonp_' + name,
globalObject: 'window',
},
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' }, // 主应用跨域抓 HTML
historyApiFallback: true,
},
};
5.3 四个高频坑
| 现象 | 原因 | 验收动作 |
|---|---|---|
| 子应用 chunk 404 | 静态资源未开 CORS,或没注入 public path | 子应用既能独立访问、也能嵌入访问 |
| 刷新子路由跳回主应用 | 路由 base 与 activeRule 不一致;根 DOM id 没限制在 container 内查找 |
直接刷新 /shop/detail 应停在子应用 |
| 来回切换后页面越来越卡 | unmount 没真正释放实例与定时器,二次进入重复挂载 |
来回切换十次,全局定时器与 DOM 节点数量不增长 |
| 弹窗或全局 reset 串样式 | 2.x 的样式隔离不是硬边界,第三方库注入的样式仍可能越界 | 把弹窗挂到子应用容器内,验收全局 reset 不污染主应用 |
照抄旧教程还容易踩一个:webpack 5 要写 chunkLoadingGlobal,jsonpFunction 是 webpack 4 的写法。
5.4 3.0 先放观察清单,别进生产计划
3.0 把能力拆成 @qiankunjs/loader、@qiankunjs/sandbox、@qiankunjs/shared 等包:HTML entry 改为流式注入、解析时自动预加载脚本与样式,JS 沙箱默认开启,提供 React / Vue 的 <MicroApp /> 绑定与 webpack / Vite 插件,样式隔离改成基于 CSS @scope 的 opt-in 能力。
注意两个边界:不支持 @scope 的浏览器会整块丢弃 这条 at-rule,需要普通回退样式;原生 ESM 沙箱依赖动态注入 import maps,官方在 Firefox 上标注了预期失败。这些都是 RC 线当前的实现,不要写成 2.x 已经有的能力。
六、六个场景:什么信号出现时才该动架构
下面每个场景都按「信号 → 为什么 → 路线 → 一条可验证判据」写。判据比结论重要,因为它能在你们自己的环境里被证伪。
S1 多业务域的企业中后台
- 信号:一个门户里挂着订单、财务、客服、报表,三支以上团队各自排期。
- 为什么:域边界天然清晰,发布互相卡是最贵的成本。
- 路线 :MF(同技术栈)或 qiankun(异构)。用 monorepo 分目录不解决发布耦合,
exposes或registerMicroApps才是把交付边界切开的那一刀。 - 判据:能否指出两个上个月因对方未发版而推迟的需求。
S2 遗留系统渐进迁移
- 信号:jQuery / AngularJS 老系统要换 Vue 或 React,但不能停机重写。
- 为什么:按路由一颗颗替换,老页面继续跑,新页面用新栈,随时可回退。
- 路线:qiankun 应用级。HTML entry 对老页面侵入最小,子应用连打包器都不用换;MF 要求 shared 协商,老系统很难配合。
- 判据:新旧页面共存期间,登录态与权限是否都能正确传递。
S3 多团队独立发布
- 信号:每周只有一个发布窗口,任何一个团队改一行都要全量回归。
- 为什么:把发布流水线拆开,回归范围收敛到自己的域。
- 路线:MF + manifest。远端入口是一个 URL,可以做灰度切远端版本,回滚时换回旧 manifest 即可,不用重建主站。
- 判据:单个团队发版时,其他团队是否还需要陪跑回归。
S4 并购或跨部门系统集成
- 信号:两套现成系统要出现在同一导航下,技术栈与框架版本都不同。
- 为什么:不改对方代码,只补生命周期与隔离,接入成本最低。
- 路线:qiankun;隔离要求极高时直接用 iframe 换硬边界。
- 判据 :两个系统的全局 CSS 与
window变量是否互不干扰。
S5 大型组件 / 营销位按域发布
- 信号:设计系统、活动页、插件市场由独立团队维护,主站不希望跟着重发。
- 为什么:远端重新部署后主站无需重新构建,配合 manifest 可做 A/B 与按时段切换。
- 路线:MF 2.0,模块粒度最贴合;这类需求用应用级方案等于为了挂一个 Banner 起一个子应用。
- 判据:远端发版后,主站是否真的不需要重新构建。
S6 多租户与白标投放
- 信号:不同客户看到不同功能模块,且要按租户灰度。
- 为什么:运行时按租户解析不同的 remote 地址,不必为每个客户打一份包。
- 路线:MF + runtime plugin / manifest 动态解析。注意这要求把「租户 → 模块版本」做成可下发配置,并给解析失败留默认模块。
- 判据:切换租户配置后,不重新构建就能换掉模块。
六个场景里有一条共同的技术底座:入口必须是一个可版本化、可回滚的 URL。做不到这点,独立部署就只是把发布窗口从主站挪到了 CDN。
七、三个反例:这时候请叫停
- 单团队小产品:一个仓库、一个团队、发布从来不排队。没有协作问题可解,只多几套构建和一份运行时,多份框架运行时还会重复下发到浏览器。
- 只想复用几个组件:诉求是「别把 Button 复制三遍」。内部 npm 包或 monorepo 共享包足够,构建时确定版本,类型、Tree Shaking、调试都更省事;运行时协商反而让这三项变差。
- 首屏性能是硬指标:LCP 有硬约束、弱网用户占比高、页面几乎无交互。运行时组合带来额外请求瀑布与重复依赖,收益抵不过延迟。
更简单的替代方案:
| 如果真实需求是 | 更简单做法 | 为什么 |
|---|---|---|
| 复用组件 / 设计系统 | monorepo 内部包 + 私服 npm | 构建时确定版本,类型、Tree Shaking、调试都最省事 |
| 彻底隔离一个第三方页面 | 原生 iframe | 硬隔离零配置,代价是布局、通信与登录态体验 |
| 按域独立部署但同技术栈 | MF + manifest | 共享一份框架运行时,比整应用沙箱更轻 |
| 框架无关的标准组件 | Web Components / import maps | 浏览器原生,不绑定打包器 |
| 按路由在服务端拼页面 | 服务端组合 / 边缘包含 | 首屏 HTML 完整,SEO 与性能更可控 |
行业文章常写「三支以上独立团队才值得上微前端」,这类数字来自工程实践总结而非官方规范,只当参考,别当准入标准。真正该量的是发布冲突次数。
八、上手就会踩的版本坑
数据核实于 2026-11-13,只认 npm dist-tag 与官方站点:
| 项目 | 当前版本 / 状态 | 说明 |
|---|---|---|
| Module Federation 2.0 | 稳定版 2026-02-10 | 官方博客宣布稳定;npm 上 enhanced 2.0.0 为 02-06 |
| @module-federation/enhanced | 2.9.3(2026-11-03) | latest;提供 ./webpack、./rspack、./runtime 子路径 |
| @module-federation/vite | 1.23.11 | Vite ESM remote 需显式 type: 'module' |
| webpack / Rspack / Vite | 5.112.2 / 2.3.2 / 8.3.8 | npm dist-tag latest |
| qiankun | latest 2.10.16(2023-11-15) | 默认 npm i qiankun 装到的就是它 |
| qiankun 3.0 | rc 3.0.0-rc.22(2026-08-30) | 仍在 active development;安装需 npm i qiankun@rc |
| single-spa | 6.0.3(2024-09-29) | qiankun 2.x 的底座 |
五条最容易写错的旧说法:
| 常见旧说法 | 当前口径 |
|---|---|
| 「qiankun 3.0 已经稳定,直接用最新版」 | 3.0 仍是 RC;latest 是 2.10.16,装 3.0 要显式 @rc |
| 「Module Federation 是 Webpack 5 专属功能」 | 2.0 已解绑打包器,覆盖 Webpack / Rspack / Rollup / Rolldown / Vite / Metro |
| 「MF 2.0 是 2026 年才发布的新东西」 | 2024-04-26 首发公告,2026-02-10 才宣布 stable,两件事不能混 |
| 「MF 自带 JS 沙箱,可以放心接第三方」 | MF 不隔离全局;沙箱是 qiankun 的能力 |
| 「singleton: true 就解决了版本冲突」 | 只保证加载一份;版本不满足时是警告 + 取现有最小版本 |
两条时间线值得单独记住:MF 2.0 2024-04-26 首发、2026-02-10 稳定 ;qiankun 2.10.16 停在 2023-11-15 ,3.0 从 2023 年就在 RC,直到 2026 年(rc.21 于 02-04、rc.22 于 08-30)才重新推进,latest 至今仍指向 2.x。写方案时把「首发」当「稳定」、把 rc 当 latest,评审会上很容易被问住。
九、上线前清单与三问决策表
上线前把这几项过一遍,比选对框架更能决定结果:
- 先选一个低风险、有真实流量、有明确负责人的页面做试点,不要拿登录、结算、支付开刀。
- 把设计系统、鉴权、路由、埋点、错误处理、无障碍六项的归属写清楚,再拆代码。
- 共享依赖版本范围写成显式契约,CI 里加一条「版本漂移」检查。
- 每个远端 / 子应用都要有超时、重试、降级 UI 与错误边界,并在演练中真的断一次网。
- 监控按应用名 + 版本号打标,能回答「这次报错是哪个团队哪个版本引入的」。
- CDN 路径与 manifest / HTML entry 要可版本化、可回滚,禁止覆盖式发布。
- 跑一遍生产构建(不是 dev server):CSS、chunk 路径、CORS 头、缓存策略逐项验收。
- 量一次改造前后的首屏 JS 体积与 LCP,确认重复依赖没有把性能吃掉;再准备一份「退回单体」的预案。
三问定路线:
- 要拆的是模块还是整应用? 模块级走 MF,应用级走 qiankun。
- 必须让不同框架或同一框架的两个大版本同页跑吗? 必须,就选带沙箱的应用级方案;不必须,MF 更轻。
- 能约束所有团队用同一套共享依赖版本吗? 能,MF 的
shared才有意义;不能,别假装singleton能替你做版本治理。
| 情况 | 建议路线 | 理由 |
|---|---|---|
| 统一技术栈、按域独立发版 | Module Federation 2.0 | 共享一份运行时,粒度最贴合模块与页面片段 |
| 异构存量系统渐进迁移 | qiankun 2.10.16 | HTML entry 对老页面侵入最小,沙箱容纳不同框架 |
| 完全不可信的第三方页面 | iframe | 硬隔离优先,接受通信与布局代价 |
| 只需要复用组件 | monorepo 内部包 | 构建时组合更简单,类型与调试都更好 |
| Vite 8 新项目、必须联邦 | @module-federation/vite | 先在生产构建验收 CSS 与 chunk 路径 |
| 暂无独立部署诉求 | 不要上微前端 | 先量团队数与发布冲突,再谈架构 |
2026 年的务实建议:新项目、统一技术栈、多团队要独立发版,优先 MF 2.0 + Rspack 或 Webpack 5 ,用 mf-manifest.json 并打开共享依赖 Tree Shaking;Vite 项目可用 @module-federation/vite,但先在生产构建里验一遍 CSS 与 chunk。存量异构系统要渐进迁移、或者必须混跑框架,用 qiankun 2.10.16 稳定线,把 3.0 放进观察清单而不是生产计划。
十、小结
微前端不是「更先进的前端架构」,而是一笔组织协作的投资:用运行时复杂度换发布自主权。买之前先量发布冲突,买之后先把 URL 契约、版本治理、降级与监控做完。
- 拆模块、同技术栈、要独立发版:MF 2.0。
- 拆整应用、异构存量、要隔离:qiankun 2.10.16。
- 没有独立部署诉求:monorepo,别折腾。
参考资料(均为官方来源,2026-11-13 核实):
- Module Federation 官方文档与 2.0 稳定版公告:module-federation.io/
- module-federation/core README:github.com/module-fede...
- qiankun 2.x 文档:v2.qiankun.umijs.org/
- umijs/qiankun README 与 3.x 分支说明:github.com/umijs/qiank...
- micro-frontends.org 定义:micro-frontends.org/
- 各包版本与发布日期(npm registry 的 dist-tag 与
time字段):registry.npmjs.org/
你们团队的发布窗口是怎么卡的、最后选了哪条路线?评论区聊聊,尤其是踩过 shared 版本漂移和样式越界的,那两个坑最值得交换经验。