楼层: 首页/ 软件技术/ 前端进阶/ 微前端与 Module Federation:让多个团队独立发布
微

微前端与 Module Federation:让多个团队独立发布

Micro Frontends · Webpack 5 Module Federation / qiankun / vite-plugin-federation

微前端是一个非常容易被"用错"的技术。它解决的核心问题只有一个:当一个前端应用大到几个团队同时改、天天互相阻塞、发布要排队协调的时候,怎么让每个团队独立开发、独立部署。注意,它不解决"代码太乱""性能太差""技术栈太旧"——那三个问题各有各的解法,用微前端去治,只会把复杂度乘二。这一章先把"该不该用"讲清楚,再讲怎么用,以及那些让人半夜爬起来回滚的坑。

微前端到底解决什么问题

先说问题的真实形态。一个前端项目长到一定规模,会出现三个具体症状。

论三个症状,只有第一个是微前端的靶心

① 发布互相阻塞(这是靶心):订单团队改了一行代码,要等积分团队、报表团队都测完才能一起发。大家都被绑在同一个发布列车上,任何一个人的问题都会拖住所有人。微前端要解决的就是这个:每个子应用有自己的仓库、自己的流水线、自己的发布时间。

② 技术栈无法演进(这是连带收益):老模块用 React 16 + class 组件,新模块想用 React 19 + Server Components。放在一个工程里,升级等于全量重写;拆成子应用,可以一个新一个旧,慢慢换。

③ 代码耦合导致的"改一处崩一片"(这个不该用微前端治):如果根源是模块划分混乱、共享状态满天飞,那需要的是重构目录结构与抽公共库,而不是拆成多个运行时应用。拆成子应用之后,耦合只是从"编译期"变成了"运行期",问题依然在,而且更难查。

应该考虑微前端不该用微前端
多个团队(比如 3 个以上)在同一个前端产品上协作一个团队(1~5 人)维护的前端,无论多大
各模块的发布节奏差异明显,被互相阻塞只是"代码乱",发布节奏本来就能协调
需要不同模块用不同技术栈、或需要渐进式技术升级仅仅为了"用新技术"而拆
产品本身是"门户型",由多个相对独立的业务模块拼成(如企业后台、运营平台)产品是强耦合的单一体验(如一个复杂的编辑器),模块之间交互极密集
已有存量老系统,要用"绞杀者模式"逐块替换项目刚开始,还没跑通业务
先问一句:你能不能先做单体分包?

在动手拆微前端之前,请先认真评估一个更简单的方案:保持一个应用,但用 monorepo 管理多个包,各包有独立的负责人,通过构建工具做代码分割与按需加载。这个方案能解决"代码怎么分"和"构建怎么快",只有在"发布必须解耦"这一条上差一点。而发布解耦的收益,往往可以用"更短的发布周期 + 更好的自动化测试"来部分获得。微前端引入的运行期复杂度(依赖共享、样式隔离、通信、版本管理、调试困难)是很高的,只有在"多团队独立发布"这个收益足够大时才值得付。很多公司上了微前端之后悔的,多半就是这一步没算账。

五种方案对比:从 iframe 到 Module Federation

微前端不是一个库,而是一类"把多个独立应用拼成一个页面"的做法。主流方案有五种,从最土到最现代,各有它存在的理由。

方案隔离程度优点代价 / 适用场景
iframe最强。JS/CSS/DOM 完全隔离,各自独立进程上下文实现简单、天然隔离、子应用技术栈完全自由路由与刷新不同步、弹窗无法跨框、通信靠 postMessage 很繁琐、SEO 差、页面内滚动体验差。适合极少数需要强隔离的嵌入式场景(如第三方插件),不适合做主方案。
Web Components较强。Shadow DOM 隔离样式浏览器原生标准,无框架依赖样式隔离了但 JS 全局变量(window、事件)还是共享的;Shadow DOM 对第三方 UI 库(弹窗挂到 body)不友好;配套生态不成熟。适合组件级嵌入,不适合整体微前端方案。
qiankun强。JS 沙箱(代理 window)+ 样式隔离国内生态成熟、文档中文友好、接入成本低、支持 HTML Entry基于 single-spa 封装,运行期有一定性能开销;沙箱对某些库不完全兼容;依赖需要子应用自己带全,容易重复加载。适合已有多个独立仓库、技术栈混杂的中后台系统。
Module Federation弱。共享运行时,靠约定隔离构建期就解决了依赖共享,不重复加载 React;模块级复用(可以只共享一个组件);Webpack 原生能力,生态成熟需要统一构建工具基础(Webpack 5 / Rspack / Vite 插件);共享依赖的版本策略要设计好,否则出现双实例;隔离靠约定。适合技术栈统一(都是 React/Vue)、追求性能与模块复用的团队。
单一 SPA 分包无最简单、性能最好、调试最方便没有发布解耦能力。如果团队能接受"一起发布",这是最优解——不要为了架构而架构。

论qiankun 和 Module Federation 的本质差别

① qiankun 是"运行期拼装":主应用在运行时去加载子应用的 HTML/JS,在同一个页面里执行多个应用,靠沙箱把它们的全局影响隔离开。子应用是完整的应用,各自带自己的框架运行时。

② Module Federation 是"构建期协商 + 运行期共享":各方在构建时就声明"我导出什么、我依赖什么",运行时通过一个共享容器解析依赖。React 只加载一份、公共库只加载一份,子应用可以只是"一组模块"而不是完整应用。

③ 一句话选择:技术栈混杂、需要强隔离、已有独立 HTML 入口 → qiankun;技术栈统一、在意包体积和模块复用 → Module Federation。两者也可以混用(主应用用 MF 共享依赖,某些老模块用 qiankun 挂进来),但复杂度会上升,非必要不混。

Module Federation 配置与加载原理

Module Federation 的核心只有三个配置项:exposes(我给别人什么)、remotes(我用别人的什么)、shared(我们共用哪些库)。先把它们配好,再理解运行期发生了什么。

两个应用:remote(提供方)与 host(消费方)

// ================= remote 应用(比如积分模块)的配置 ================= // apps/points/webpack.config.js const { ModuleFederationPlugin } = require("webpack".container); const deps = require("./package.json").dependencies; module.exports = { mode: "production", output: { // 关键:让远程 chunk 知道自己从哪个域名加载(否则跨域加载会算错路径) publicPath: "https://points.example.com/", uniqueName: "pointsApp", // 避免多个应用在全局作用域里撞名 }, plugins: [ new ModuleFederationPlugin({ name: "pointsApp", // 远程容器名,host 靠这个名字找它 filename: "remoteEntry.js", // 生成的入口清单文件 // exposes:我对外暴露哪些模块 exposes: { "./PointsPanel": "./src/PointsPanel.tsx", // 积分面板组件 "./usePoints": "./src/hooks/usePoints.ts", // 也可以暴露一个 hook }, // shared:这些库和 host 共用一份,不要各打一份 shared: { react: { singleton: true, // 全页面只能有一份 React 实例,必须开 requiredVersion: deps.react, eager: false, // 懒加载,别打进初始 chunk }, "react-dom": { singleton: true, requiredVersion: deps["react-dom"] }, "react-router-dom": { singleton: true }, "zustand": { singleton: true }, }, }), ], }; // ================= host 应用(主应用)的配置 ================= new ModuleFederationPlugin({ name: "mainApp", remotes: { // 语法:"容器名@远程地址/清单文件名" pointsApp: "pointsApp@https://points.example.com/remoteEntry.js", // 也可以做成运行时动态注入,便于按环境切换(见下一段) orderApp: "orderApp@https://order.example.com/remoteEntry.js", }, shared: { react: { singleton: true, requiredVersion: deps.react }, "react-dom": { singleton: true }, "react-router-dom": { singleton: true }, }, });

host 里怎么用远程模块:静态引入 vs 懒加载(推荐)

import React, { Suspense, lazy } from "react"; // ---------- 方式一:静态引入,会打进初始包,不推荐 ---------- // 依赖的远程应用一挂,主应用整体就起不来 // import PointsPanel from "pointsApp/PointsPanel"; // ---------- 方式二:懒加载(推荐):用 React.lazy 包一层 ---------- // 注意:lazy 要求模块导出 default,所以远程模块最好提供 default 导出 const PointsPanel = lazy(() => import("pointsApp/PointsPanel")); const OrderList = lazy(() => import("orderApp/OrderList")); // 远程模块加载失败时的兜底:用 ErrorBoundary 包住,避免整个页面白屏 class RemoteBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return <div className="remote-fallback">该模块暂时不可用,请稍后再试</div>; } return this.props.children; } } export default function App() { return ( <RemoteBoundary> <Suspense fallback={<div>加载中...</div>}> <PointsPanel userId={1001} /> </Suspense> </RemoteBoundary> ); } // ---------- 方式三:运行时动态决定加载哪个远程(用于多环境 / 灰度)---------- const remoteUrl = window.__REMOTE_CONFIG__.pointsApp; const PointsPanelDynamic = lazy(async () => { // 动态构造远程入口,配合 __webpack_init_sharing__ 初始化共享作用域 await __webpack_init_sharing__("default"); const container = window.pointsApp; await container.init(__webpack_share_scopes__.default); const factory = await container.get("./PointsPanel"); return factory(); });

论运行期到底发生了什么(理解这个,坑就好排查了)

① 构建时:每个应用把"我导出哪些模块"写进 remoteEntry.js 这份清单。同时,所有被声明为 shared 的库不在应用内直接打包,而是注册成一个"可以提供/可以消费"的条目。

② 页面加载时:宿主应用先加载自己的代码,同时初始化一个共享作用域(share scope)。这个作用域本质是一张全局表:{ react: { "19.0.0": 提供者工厂 }, react-dom: {...} }。

③ 加载远程模块时:浏览器去请求 remoteEntry.js,得到一个"容器"对象。宿主调用 container.init(shareScope),把远程应用的共享需求和自己已有的共享作用域对接——这就是"React 只加载一份"的实现方式。

④ 取模块:container.get("./PointsPanel") 返回一个工厂函数,调用它才真正拿到模块。因为这一步是异步的,所以要用 lazy 包起来。⑤ 关键推论:共享依赖的版本匹配在运行期发生,配置写错不会在构建时报错,只会运行时出现各种诡异现象——这就是后面"双实例"坑的根源。

Vite 生态:vite-plugin-federation

Module Federation 最初是 Webpack 的能力。如果项目用 Vite(尤其是 Rollup 构建的生产版本),需要额外的插件来复刻这套机制。这就是 @originjs/vite-plugin-federation(社区常称 vite-plugin-federation)。

用 Vite 插件实现等价能力,以及它的限制

// ================= remote 应用:vite.config.ts ================= import { defineConfig } from "vite"; import federation from "@originjs/vite-plugin-federation"; export default defineConfig({ plugins: [ federation({ name: "pointsApp", filename: "remoteEntry.js", exposes: { "./PointsPanel": "./src/PointsPanel.tsx", }, shared: { react: { singleton: true, requiredVersion: "^19.0.0" }, "react-dom": { singleton: true, requiredVersion: "^19.0.0" }, "react-router-dom": { singleton: true }, }, }), ], build: { target: "esnext", // 必须:联邦机制依赖顶层 await 与动态 import minify: false, // 官方建议在联邦场景下先关掉压缩,避免变量名被破坏 cssCodeSplit: true, modulePreload: false, // 关掉预加载,避免加载到未初始化的共享模块 }, }); // ================= host 应用:vite.config.ts ================= federation({ name: "mainApp", remotes: { pointsApp: { external: "https://points.example.com/assets/remoteEntry.js", format: "esm", // Vite 场景下用 ESM 格式 from: "vite", }, }, shared: ["react", "react-dom", "react-router-dom"], }); // ============ 三个必须知道的限制 ============ // 1) dev 模式支持有限:vite-plugin-federation 在 dev server 下对远程模块的支持不完整 // 常见做法是"本地开发用 build + preview 验证联邦功能",日常开发用打包产物调试 // 2) 产物必须是 ESM:target 要设成 esnext,老浏览器(如 IE、部分低版本)不支持 // 3) HMR 不能跨应用:改了 remote 的代码,host 页面不会自动热更新,需要手动刷新 // 这一点对开发体验影响明显,要提前和团队说清楚
对比项Webpack 5 Module Federationvite-plugin-federation
成熟度官方能力,生产验证充分,文档与案例多社区插件,功能覆盖较全但边界情况更多
开发体验dev server 直接支持远程模块,HMR 基本可用dev 模式支持有限,常需用构建产物调试
产物格式可输出 UMD/ESM,兼容老浏览器必须 ESM + esnext,需要现代浏览器
压缩可在联邦场景下正常工作官方建议先关 minify,包体会变大
建议如果项目已经是 Webpack 5 / Rspack,直接用原生能力;如果新项目用 Vite 且确实需要微前端,评估插件限制后再决定,必要时把"共享依赖"退化成"各自打包 + 外置 CDN"

共享依赖版本策略、样式与路由隔离

Module Federation 的隔离是"弱隔离"——共享运行时意味着大家在一个全局环境里。所以版本策略、样式、路由这三件事必须提前约定好,否则会在上线后以很奇怪的方式暴露出来。

版本不一致时会怎样:用配置演示"双实例"的产生

// ============ 场景:host 用 React 19,remote 声明需要 React 18 ============ // remote 的配置(错误示范) shared: { react: { requiredVersion: "^18.0.0" }, // 没写 singleton } // 结果:版本区间不满足,联邦机制会为 remote 单独加载一份 React 18 // 于是页面上同时存在两份 React → 所谓的"双实例"问题 // ============ 双实例会引发的三个典型报错 ============ // 1) Invalid hook call. Hooks can only be called inside of the body of a function component. // (因为 remote 组件用的是 React 18 的 hooks 实现,而渲染由 React 19 驱动) // 2) Context 读不到:HostProvider 提供的 Context,remote 组件里 useContext 拿到 null // (两份 React 各自有独立的 Context 注册表) // 3) useState 更新了但视图不刷新 // (调度器是两套,状态更新没有被渲染器感知) // ============ 正确做法:singleton + 统一的版本区间 ============ shared: { react: { singleton: true, // 只允许一份,强制复用已加载的 requiredVersion: "^19.0.0", // 所有应用对齐到同一个主版本 strictVersion: false, // 版本不匹配时不直接抛错(见下方说明) }, "react-dom": { singleton: true, requiredVersion: "^19.0.0" }, "react-router-dom": { singleton: true }, } // ============ strictVersion 到底该不该开?============ // true :版本不匹配时直接抛错,问题立刻暴露,但可能整个页面打不开 // false :不抛错,用"最接近"的版本继续跑,风险是静默地用了不兼容的版本 // 建议:预发环境开 true(尽早暴露),生产环境开 false(保证可用性), // 同时把"版本不一致"上报到监控,作为技术债跟踪 // ============ 更稳的做法:用构建时校验,把问题挡在上线前 ============ // 在 CI 里加一步:比对各应用 package.json 里 shared 依赖的主版本是否一致 // 不一致就构建失败,避免"配置写错 → 运行期才炸"
隔离维度Module Federation(弱隔离)qiankun(强隔离)
JS 全局变量共享同一个 window,约定不改全局。子应用污染全局会互相影响用 Proxy 代理 window,子应用的全局修改被限制在自己的沙箱里
样式无自动隔离。必须靠约定:CSS Modules、CSS-in-JS、或统一前缀支持沙箱(给子应用的样式加作用域选择器)和 Shadow DOM 两种模式
路由无隔离。通常是"host 拥有唯一 Router,remote 只暴露组件不自己管路由"子应用可以有自己的 Router,路由变化会通知主应用(需处理冲突)
定时器/事件无隔离,卸载时自己清理沙箱会记录并回收部分副作用,但仍需子应用配合

路由与通信:主应用管路由,子应用只暴露组件 + 通过 props 通信

// ================= 路由:约定"host 拥有唯一 Router" ================= // ❌ 错误做法:remote 里也 createBrowserRouter(),两份 Router 抢同一个 history // ✅ 正确做法:remote 只导出"页面组件",路由挂载由 host 负责 // ---- host 侧:统一路由表,把 remote 组件挂到对应 path 上 ---- import { createBrowserRouter, RouterProvider } from "react-router-dom"; const router = createBrowserRouter([ { path: "/", element: <Layout />, children: [ { path: "points", element: <PointsPanel /> }, // 来自 remote { path: "orders", element: <OrderList /> }, // 来自另一个 remote ]}, ]); // ---- remote 侧:不创建 Router,只导出组件;需要跳转就从 props 拿 ---- // apps/points/src/PointsPanel.tsx export default function PointsPanel({ userId, onNavigate }: { userId: number; onNavigate?: (path: string) => void; // 由 host 注入的跳转能力 }) { return ( <div> <h2>积分:{userId}</h2> <button onClick={() => onNavigate?.("/orders")}>去看订单</button> </div> ); } // ================= 三种通信方式,按耦合度递增排序 ================= // 方式一(推荐):props 传参。最清晰,类型可以共享,也最容易测试 <PointsPanel userId={currentUser.id} onNavigate={navigate} /> // 方式二:事件总线。适合"松耦合的通知",比如"用户登出了,所有子应用清空缓存" // 定义一个全局事件名约定,host 和 remote 都 import 同一份事件常量包 window.dispatchEvent(new CustomEvent("app:logout")); // remote 侧订阅(注意:组件卸载时必须 removeEventListener) useEffect(() => { const handler = () => clearLocalCache(); window.addEventListener("app:logout", handler); return () => window.removeEventListener("app:logout", handler); }, []); // 方式三:全局状态(慎用)。让 remote 直接读 host 的 store // 前提是把 store 声明成 shared singleton,让两边拿到同一个实例 // 风险:remote 和 host 的 state 形状强耦合,独立部署时容易不匹配 // 建议:只在"共享的是身份/权限/主题这类稳定数据"时使用

独立部署、灰度与联调

微前端真正带来价值的地方在这里:子应用可以独立发布。但独立发布同时意味着"任何一方上线都会立刻影响线上",所以必须有配套的版本管理与回滚机制。

论三种版本管理策略

① 引用固定 URL(最简单,最不稳):remotes: { pointsApp: "https://points.example.com/remoteEntry.js" }。remote 一发版,所有 host 立刻生效。风险:remote 的新版本有 bug,host 只能靠 remote 回滚;而且多环境(测试/生产)不好切。

② 带版本号或 hash(推荐):每个发布产物放到独立目录,例如 /releases/2026-09-20-a1b2c3/remoteEntry.js。host 引用哪个版本由配置下发决定:window.__REMOTE_CONFIG__.pointsApp。这样能做到"灰度把 5% 的 host 用户指向新版本"。

③ 版本矩阵(最规范,成本最高):维护一张"host 版本 → remote 版本"的兼容表,发布时校验组合,并在返回值里带上版本,出问题时能快速定位"是哪个组合出的问题"。适合子应用多、发布频繁的成熟阶段。

配置下发 + 灰度:让远程版本可控

// ================= 1) 把远程地址做成运行时配置,不打进包里 ================= // public/runtime-config.js —— 部署时由运维修改,无需重新构建前端 window.__REMOTE_CONFIG__ = { pointsApp: "https://points.example.com/releases/2026-09-20-a1b2c3/remoteEntry.js", orderApp: "https://order.example.com/releases/2026-09-18-f9e8d7/remoteEntry.js", }; // index.html 里在业务代码之前加载它 // <script src="/runtime-config.js"></script> // ================= 2) 灰度:按用户维度决定用哪个版本 ================= // 灰度逻辑放在网关 / BFF 层,前端只消费结果 function pickRemoteUrl(userId: string) { const stables = "https://points.example.com/releases/2026-09-18-aaa/remoteEntry.js"; const canary = "https://points.example.com/releases/2026-09-20-bbb/remoteEntry.js"; // 稳定的哈希分桶:同一个用户每次都进同一个桶,体验一致 const bucket = hashToBucket(userId, 100); return bucket < 5 ? canary : stables; // 5% 用户走新版本 } // ================= 3) 兜底:远程加载失败时降级到上一个稳定版本 ================= async function loadRemoteWithFallback(primary: string, fallback: string) { try { return await loadRemote(primary); } catch (err) { reportError("remote_load_failed", { primary, err }); // 上报监控 return await loadRemote(fallback); // 兜底到稳定版本 } } // 注意:兜底只能在"两个版本的接口完全兼容"时使用 // 否则会从"页面报错"变成"数据错乱",后者更难发现。保险的做法是兜底到占位组件 // ================= 4) 联调:本地同时起多个应用 ================= # package.json 脚本:一条命令把 host 和两个 remote 一起拉起来 "dev:all": "concurrently -n main,points,order -c blue,green,yellow \"pnpm -F main dev\" \"pnpm -F points dev\" \"pnpm -F order dev\"" # 然后用 localhost 的 runtime-config 覆盖远程地址,指向本地端口 # window.__REMOTE_CONFIG__ = { pointsApp: "http://localhost:3001/remoteEntry.js" } # 这样改 remote 的代码能立刻在 host 里看到(Webpack 场景下配合 HMR 体验最好)
两个最常被踩的坑

坑一:React 双实例导致 hooks 报错。现象是控制台出现 Invalid hook call,或者 useContext 拿到 null,或者状态更新了界面不刷新。原因:host 和 remote 各自加载了一份 React(因为 singleton 没开、或版本区间不满足)。排查三步:① 打开 React DevTools,看 Component 树里是不是有两棵根;② 在页面里打印 React.version 和 React.__SECRET_INTERNALS... 的引用是否一致;③ 检查 host 和 remote 的 shared.react 是否都写了 singleton: true 且版本区间有交集。根治办法:所有应用到同一个 React 主版本,CI 里加版本一致性校验。

坑二:子应用卸载后不清理定时器与事件监听。组件已经被卸载,但 setInterval 还在跑、window.addEventListener 还挂着、WebSocket 还连着。后果是内存泄漏、以及"用户已经离开页面但还在发请求"的诡异现象,在微前端里尤其严重,因为卸载重挂的频率比单应用高得多(路由切换、版本切换都会触发)。规矩:任何在 useEffect/onMounted 里建立的副作用,都必须在返回的清理函数里释放——clearInterval、removeEventListener、controller.abort()、socket.close(),一个都不能漏。另外如果是 qiankun 这类平台,还要在 unmount 生命周期里做一次兜底清理。

记
本章小结

① 微前端只解决"多团队独立开发与发布",不要用它治代码混乱、性能差、技术栈旧这些别的问题。

② 上微前端前先认真评估"单应用 + 分包 + monorepo",它的复杂度低一个数量级。

③ 五种方案:iframe/Web Components 隔离强但体验差;qiankun 强隔离、适合混杂栈;Module Federation 依赖共享、适合统一栈;能不分包就不分包。

④ Module Federation 三个核心配置:exposes 我给别人什么、remotes 我用别人什么、shared 我们共用哪些。

⑤ 运行期靠 share scope 共享依赖——所以版本配置写错不会在构建期报错,只在运行时炸。

⑥ react/react-dom/路由库必须 singleton: true 且主版本对齐,否则出现双实例,报 Invalid hook call。

⑦ 路由由 host 唯一持有,remote 只暴露组件;通信优先 props,其次事件总线,全局状态慎用。

⑧ 远程地址做成运行时配置,配合灰度分桶与失败兜底,才有独立发布的安全性。

⑨ 远程模块一律懒加载 + Suspense + ErrorBoundary,绝不能让一个子应用的故障白屏整个页面。

本章自测

自测 · 微前端与 Module Federation

1.(判断题)我们前端项目有 30 万行代码、代码很乱、构建要 5 分钟。应该上微前端吗?

看答案

答案:先别急,这个描述里没有"多团队被发布互相阻塞"这个关键症状。30 万行、代码乱、构建慢,对应的解法是:① 目录与模块划分重构;② 抽公共库、收敛依赖;③ 构建优化(缓存、增量、并行、换用更快的构建器)。这些做完,构建时间能降一大半,而且没有引入运行期复杂度。只有当"多个团队必须独立发布"这个需求无法用别的方式满足时,才考虑微前端。用微前端治代码乱,等于把编译期的耦合推迟到运行期,问题更难查。

2.(排查题)接入 Module Federation 后,页面报 Invalid hook call。列出你的排查步骤。

看答案

答案:这是典型的 React 双实例。排查步骤:① 先确认是不是双实例——在 remote 组件和 host 组件里分别 console.log(React.version),并在同一处打印一个挂在 React 对象上的随机标识,看两边是否同一个对象;② 检查 host 和 remote 的 shared.react 是否都配了 singleton: true;③ 检查两边的 requiredVersion 是否有交集(比如一边 ^18.0.0、一边 ^19.0.0,不满足就只能各加载一份);④ 检查是不是有间接依赖偷偷带进了另一份 React(比如某个组件库把 react 声明成了 dependency 而不是 peerDependency)。修复:把 shared 配齐、统一主版本,并在 CI 里加"共享依赖版本一致性"的校验步骤,防止以后又被改坏。

3.(设计题)主应用要往子应用传"当前登录用户"和"跳转能力",你会怎么设计?为什么不用全局状态?

看答案

答案:优先用 props 传参:<PointsPanel user={currentUser} onNavigate={navigate} />。理由:① 显式——数据从哪来、有哪些依赖,看组件签名就知道;② 可测试——单测里直接传假数据即可,不需要搭全局环境;③ 版本兼容好——子应用独立部署时,props 的类型约定比"store 的形状"更容易保证兼容。全局状态的问题:它让 remote 和 host 的内部数据结构强耦合,一边改了 shape,另一边在运行期才知道;而且全局 store 要做成 shared singleton 才能共享,又增加了双实例风险。合适的用法:只把"身份、权限、主题"这类长期稳定、双方都需要的数据放进全局状态;业务数据一律走 props。事件总线适合"通知"(如登出),不适合传数据。

4.(设计题)子应用已经上线到生产,结果发现它有个 bug。主应用需要立刻回滚吗?

看答案

答案:不需要,这正是微前端的价值——子应用独立回滚。前提是你没有把远程地址硬编码进主应用的构建产物,而是用运行时配置下发。回滚动作:把 runtime-config.js 里 pointsApp 的地址改回上一个发布产物(带 hash 的独立目录),用户刷新页面即生效,主应用完全不用动。这也是为什么推荐"每次发布产物放到独立目录、带版本号"——如果只有一份 remoteEntry.js 被覆盖,回滚就只能重新构建部署,快不了。更进一步,配合灰度分桶,可以先把新版本流量降到 0,观察后再决定是否彻底回滚。