外观
微前端落地:Webpack 5 Module Federation 实践
微前端要解决的不是"技术栈统一",而是组织协作问题:多个团队维护的大型应用,能否在不互相阻塞的情况下独立开发、独立构建、独立发布。
传统方案里,要么把代码合并进一个仓库,发布节奏被绑死;要么用 iframe,隔离彻底但体验和通信成本高。Webpack 5 的 Module Federation(简称 MF)提供了一种新思路:构建时解耦,运行时组装。
Module Federation 的核心模型
MF 把应用分成两类角色:
- Host(宿主):运行时加载远程模块,负责整体布局、路由和基础能力。
- Remote(远程):独立构建并部署,对外暴露若干模块。
每个 Remote 构建后会生成一个 remoteEntry.js,它记录了模块清单和共享依赖信息。Host 在运行时加载这个清单,按需获取模块工厂函数并执行。
text
host (独立部署)
└── 运行时加载 remoteEntry.js
├── exposes: ./Dashboard
├── exposes: ./UserList
└── shared: vue / vue-router
remote (独立部署,可独立发版)关键点在于:Host 和 Remote 之间传递的是运行时代码,不是构建期源码。这意味着 Remote 发版后,Host 不需要重新构建就能拿到新版本。
Remote 端配置
js
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
output: {
publicPath: 'auto',
uniqueName: 'remoteApp'
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard.vue',
'./UserList': './src/UserList.vue'
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
'vue-router': { singleton: true, requiredVersion: '^4.2.0' }
}
})
]
}几个容易配错的点:
output.publicPath: 'auto'让 Remote 根据自身remoteEntry.js的位置推断资源路径,部署到 CDN 子路径时尤其重要。uniqueName避免多个应用在全局共享作用域里命名冲突。singleton: true保证 Vue、React 这类有全局状态的库只加载一份。如果 Host 和 Remote 各加载一份 Vue,provide/inject、响应式系统和组件实例都会出现难以排查的问题。
Host 端配置
js
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: 'remoteApp@https://cdn.example.com/remote/remoteEntry.js'
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
'vue-router': { singleton: true, requiredVersion: '^4.2.0' }
}
})
]
}remotes 的值由三部分组成:<全局变量名>@<remoteEntry 地址>。Host 构建时不会下载远程代码,只在产物里记录这个地址。
Host 中消费远程组件:
ts
// src/pages/DashboardPage.vue
import { defineAsyncComponent } from 'vue'
const RemoteDashboard = defineAsyncComponent(() => import('remoteApp/Dashboard'))TypeScript 需要声明远程模块:
ts
// src/types/remotes.d.ts
declare module 'remoteApp/Dashboard' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}声明只能保证编译通过,运行时契约仍需团队约定:Remote 暴露的组件接收哪些 props、触发哪些事件、依赖哪些全局上下文。
共享依赖的版本策略
这是微前端最容易出事故的地方。假设 Host 使用 Vue 3.4,Remote 使用 Vue 3.5:
- 开启
singleton时,运行时只能选择一份,版本不匹配会产生警告,严重时导致 API 不存在。 - 不开启
singleton时,两份 Vue 共存,组件实例跨边界传递会失效。
推荐策略:
- 框架与路由设为 singleton,版本范围写宽一些,例如
^3.4.0。 - 开发环境开启
strictVersion: true,版本不兼容直接报错,尽早暴露问题。 - 共享 UI 基础库要谨慎。组件库往往同时依赖框架和样式,共享后升级会牵连多个团队。
- 业务工具库不共享。lodash、dayjs 这类库重复加载的成本远低于版本协调成本。
样式、路由与运行时隔离
MF 只解决模块共享,不解决隔离。
样式隔离有三种常见做法:
- CSS Modules 或 scoped 样式,成本最低,但全局样式(reset、主题变量)仍会互相影响。
- 给每个应用加根级命名空间,例如
.app-remote-dashboard下所有选择器加前缀。 - Shadow DOM,隔离最彻底,但弹窗、定位、字体继承都需要额外适配。
路由隔离建议由 Host 掌握顶层路由,Remote 只负责自己的子路由:
ts
// host/src/router.ts
{
path: '/dashboard',
component: () => import('remoteApp/Dashboard'),
children: dashboardRoutes
}如果 Remote 需要独立运行,可以给它配置 createWebHistory('/dashboard/') 的 base,并在 Host 中保持一致。不要两个应用同时操作 history 的顶层路径。
错误隔离必须做:远程资源加载失败、Remote 内部渲染异常,都不能让整个 Host 白屏。
ts
const RemoteDashboard = defineAsyncComponent({
loader: () => import('remoteApp/Dashboard'),
loadingComponent: Loading,
errorComponent: RemoteFallback,
timeout: 10_000
})Vue 侧再通过 app.config.errorHandler 上报远程模块异常,形成双重兜底。
与其它方案对比
| 方案 | 隔离性 | 体验 | 接入成本 | 适用场景 |
|---|---|---|---|---|
| iframe | 最强 | 一般,通信成本高 | 低 | 强隔离的后台工具 |
| single-spa / qiankun | 较强,有沙箱 | 较好 | 中 | 多技术栈存量应用 |
| Module Federation | 弱到中,需自行治理 | 好 | 中 | 同技术栈、需要运行时共享 |
| Web Components | 较强 | 较好 | 中 | 跨框架的可复用组件 |
MF 适合技术栈相对统一、团队协作紧密的场景。如果各团队技术栈差异很大,或者需要严格沙箱,qiankun 这类运行时方案更稳妥。
治理清单
微前端不是接完构建插件就结束了,长期维护更依赖治理:
- 契约版本化:暴露模块的 props、事件、路由约定要有文档和版本号。
- 独立发布与回滚:Remote 的
remoteEntry.js地址应支持灰度与回滚。 - 依赖白名单:明确哪些包可以共享,其余一律各自打包。
- 监控隔离:每个子应用单独上报错误、性能和资源加载失败。
- 本地开发:Host 连接本地 Remote 时需要代理 remoteEntry,避免所有联调都依赖测试环境。
2024 年以后,Module Federation 2.0、Vite 的 federation 插件以及 Rspack 原生支持逐步成熟,跨构建工具的微前端变得更现实。但共享依赖、样式隔离和发布治理仍然是核心难点,与具体工具无关。
小结
Module Federation 把微前端的粒度从"页面级集成"推进到"模块级复用",代价是运行时依赖治理更复杂。落地前先想清楚:是否真的需要独立发版?团队边界是否清晰?如果答案是否定的,单体应用加良好的模块划分,往往比微前端更划算。