怎样用 Module Federation 做微前端?和 qiankun 有什么区别?
Module Federation 在构建时声明“我暴露哪些模块、要用谁的模块、哪些依赖共享”,运行时按需加载远程模块,粒度可以细到一个组件。qiankun 基于 single-spa,以子应用为单位,通过 HTML entry 拉取子应用、调用它导出的 bootstrap/mount/unmount,并提供 JS 沙箱和可选的样式隔离。前者偏模块共享,后者偏应用级隔离与接入。
简答
订单团队维护一个订单页,主站想直接用它,又不想每次订单更新都重新打包主站。用 Module Federation,订单应用在构建配置里声明“我暴露
./OrderPage”,主站声明“我要用orders这个远程应用”,运行时主站就像写import('orders/OrderPage')一样把它加载进来。它的关键是共享依赖。双方都声明
react为共享且是单例,页面上就只加载一份 React。粒度也很灵活,可以共享一个组件、一个工具函数,也可以是整个页面。qiankun 的思路不同。它基于 single-spa,以“子应用”为单位:主应用注册子应用的入口地址,qiankun 用 fetch 拉取子应用的 HTML 和脚本,子应用导出
bootstrap、mount、unmount三个生命周期,由 qiankun 在路由匹配时调用。它默认开启 JS 沙箱,样式隔离可以选 Shadow DOM 或给选择器加前缀。所以要注意怎么选:多个应用技术栈相同、想细粒度共享代码,Module Federation 更合适,但它官方不提供样式隔离,要自己约定;要接入技术栈不同的老系统、看重应用级隔离,qiankun 更省事,代价是子应用要改打包格式并给静态资源开 CORS。
Module Federation 怎样配置
微前端适合解决什么问题、有哪几类集成方式,见《微前端适合解决什么组织和部署问题?》。这里只看 Module Federation 本身。
Module Federation 由 webpack 5 在 2019 年引入。官方文档把后来的 Module Federation 2.0 和 webpack 5 内置版本区分开:2.0 除了模块导出、加载和依赖共享,还提供动态类型提示、Manifest、Federation Runtime 和运行时插件系统,并支持 Rspack 和 webpack。
提供模块的一方叫生产者,下面是订单应用:
// 生产者:orders 应用
new ModuleFederationPlugin({
name: 'orders',
exposes: { './OrderPage': './src/OrderPage.tsx' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});
使用模块的一方叫消费者。按官方 remotes 文档,配置了 remotes 就可以视为消费者,值的格式是“生产者名称 + @ + 入口地址”,入口可以是 remoteEntry.js,也可以是 mf-manifest.json:
// 消费者:主站
new ModuleFederationPlugin({
name: 'shell',
remotes: { orders: 'orders@https://orders.example.com/mf-manifest.json' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});
// 主站代码里
const OrderPage = React.lazy(() => import('orders/OrderPage'));
入口用 mf-manifest.json 时,官方说明会额外获得动态类型提示、资源预加载和 Chrome 调试工具支持。
shared 的几个选项怎样理解
singleton 表示共享作用域里只允许一个版本。官方文档的描述是:开启后双方的共享依赖只加载一次,版本不一致时加载较高的版本,并给版本较低的一方警告;不开启时,版本不同就各自加载自己的。
requiredVersion 声明要求的版本。实际版本低于要求时,控制台会警告,并使用共享依赖里可用的最小版本。
eager 设为 true 会把共享依赖打进入口文件,入口会变大,文档提醒谨慎使用。正常情况下应该用异步入口,按需加载共享模块。
还有一个容易漏掉的坑:文档专门提醒,只把 react-dom 设为共享时,代码里 import { createRoot } from 'react-dom/client' 这种子路径导入不会自动共享,会多加载一份 React DOM,破坏 React 的单例要求。
qiankun 怎样接入
主应用注册子应用,然后启动:
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{ name: 'orders', entry: '//localhost:7100', container: '#subapp', activeRule: '/orders' },
]);
start();
子应用要在入口文件里导出三个生命周期。官方说明 bootstrap 只在初始化时调用一次,之后再进入只调用 mount:
export async function bootstrap() {}
export async function mount(props) { render(props.container); }
export async function unmount() { /* 卸载根组件,清理全局副作用 */ }
子应用的 webpack 配置还要把 output.libraryTarget 设成 'umd',library 取一个唯一的名字,qiankun 才能拿到这些导出。因为 qiankun 用 fetch 拉取子应用的资源,官方 FAQ 写明这些静态资源必须支持 CORS。
隔离方面,sandbox 默认为 true,开启 JS 沙箱。设成 { strictStyleIsolation: true } 时,qiankun 会把每个子应用的容器变成 Shadow DOM;experimentalStyleIsolation: true 则是实验性的方案,通过给子应用的选择器加上前缀来限制作用范围。
两者放在一起比
| 维度 | Module Federation | qiankun |
|---|---|---|
| 共享单位 | 模块,可以是组件、函数或页面 | 子应用 |
| 接入方式 | 构建插件声明 exposes / remotes | 注册入口地址,子应用导出生命周期 |
| 依赖共享 | shared 配置,内置版本协商 | 不提供模块级共享,各子应用自带依赖 |
| JS 隔离 | 同一个运行环境,不隔离 | 默认开启沙箱 |
| 样式隔离 | 官方不内置,靠 BEM、CSS Modules 等约定 | 可选 Shadow DOM 或选择器前缀 |
| 对子应用的改造 | 用支持的构建工具加插件 | 打包成 umd、导出生命周期、资源开 CORS |
两者也不是二选一。Module Federation 2.0 的 Bridge 能力可以加载整个应用级模块并管理挂载和卸载,官方说明支持在 React 应用里加载 Vue 应用等跨框架场景。是否值得引入,仍然取决于团队要不要独立部署、技术栈是否统一。
常见追问
Module Federation 的远程模块加载失败怎么办?
主站用
React.lazy加载时,要外包一层错误边界,显示降级内容或重试按钮,不能让整页白屏。远程应用和主站之间暴露的模块名和 props 要当作接口来维护,做到向后兼容。
qiankun 的样式隔离为什么不是默认开启?
官方 API 里
sandbox默认只开 JS 沙箱,样式隔离要显式配置。strictStyleIsolation把子应用放进 Shadow DOM,外部样式进不来、内部样式出不去,依赖全局样式的组件可能表现不同;experimentalStyleIsolation文档标明是实验性的。所以要按项目情况选择并逐页验证。
两个方案各自的共享依赖问题一样吗?
不一样。Module Federation 的风险是共享版本协商出错,加载了多份或版本不兼容;qiankun 默认各子应用自带依赖,风险是重复加载导致体积变大。