Skip to content

微前端落地: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 共存,组件实例跨边界传递会失效。

推荐策略:

  1. 框架与路由设为 singleton,版本范围写宽一些,例如 ^3.4.0。
  2. 开发环境开启 strictVersion: true,版本不兼容直接报错,尽早暴露问题。
  3. 共享 UI 基础库要谨慎。组件库往往同时依赖框架和样式,共享后升级会牵连多个团队。
  4. 业务工具库不共享。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 把微前端的粒度从"页面级集成"推进到"模块级复用",代价是运行时依赖治理更复杂。落地前先想清楚:是否真的需要独立发版?团队边界是否清晰?如果答案是否定的,单体应用加良好的模块划分,往往比微前端更划算。

基于 VitePress 构建 · 持续更新中