微
微前端与 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 Federation | vite-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,观察后再决定是否彻底回滚。