微前端与模块联邦

微前端架构将一个大型前端应用拆分为多个独立的小型应用,每个子应用可以独立开发、独立部署、独立运行。Webpack 5 引入的 Module Federation(模块联邦)是当前最优雅的微前端实现方案之一。它允许不同的 Webpack 构建产物在运行时互相消费对方的模块,无需重新打包或加载完整的远程应用。

模块联邦的核心思想是:一个应用既可以是 Host(消费者),也可以是 Remote(提供者),甚至两者兼有。这种对称的设计让微前端的组合变得极其灵活。

核心配置解析

Module Federation 通过 ModuleFederationPlugin 插件配置,其关键参数如下:

// webpack.config.js (Remote 端 — 提供模块的应用)
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      // name: 当前应用的唯一标识,Remote 端和 Host 端引用时需要一致
      name: 'remoteApp',

      // filename: 远程入口文件的名称
      filename: 'remoteEntry.js',

      // exposes: 暴露给其他应用消费的模块
      exposes: {
        './Button': './src/components/Button',
        './UserService': './src/services/UserService',
        './utils': './src/utils/index',
      },

      // shared: 声明共享依赖,避免重复加载
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        lodash: { requiredVersion: '^4.17.0' },
      },
    }),
  ],
}
// webpack.config.js (Host 端 — 消费远程模块的应用)
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',

      // remotes: 声明要消费的远程应用
      remotes: {
        // 格式: '内部引用名: 远程应用name@远程入口URL'
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },

      // Host 也应该声明 shared,确保共享依赖协商正确
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
}

Host 端使用远程模块

Host 端的入口文件需要使用异步边界(dynamic import),因为远程模块的加载是异步过程:

// src/index.js (Host 端入口)
import('./bootstrap')

// src/bootstrap.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import RemoteButton from 'remoteApp/Button'

function App() {
  return (
    <div>
      <h1>Host Application</h1>
      <RemoteButton onClick={() => alert('来自远程模块')}>
        远程按钮
      </RemoteButton>
    </div>
  )
}

createRoot(document.getElementById('root')).render(<App />)

共享依赖深入理解

共享依赖是模块联邦的关键特性。当多个应用声明了相同的 shared 依赖时,运行时只会加载版本最高的一份。配置选项的含义:

shared: {
  react: {
    singleton: true,        // 全局唯一实例
    requiredVersion: '^18.0.0',
    strictVersion: true,    // 版本不匹配时警告
    eager: false,           // 异步加载(推荐)
  },
  vuex: {
    requiredVersion: '^4.0.0',
    singleton: true,
  }
}

动态远程加载

除了在配置中静态声明 remotes,还可以在运行时动态加载远程模块,这适合需要根据环境或用户身份决定加载哪些子应用的场景:

// 动态加载远程组件
import React, { lazy, Suspense } from 'react'

function useDynamicScript(url) {
  const [ready, setReady] = React.useState(false)
  React.useEffect(() => {
    const element = document.createElement('script')
    element.src = url
    element.type = 'text/javascript'
    element.async = true
    element.onload = () => setReady(true)
    document.head.appendChild(element)
    return () => { document.head.removeChild(element) }
  }, [url])
  return ready
}

function DynamicRemoteComponent({ url, scope, module }) {
  const ready = useDynamicScript(url)

  const Component = React.useMemo(() => {
    if (!ready) return null
    return lazy(async () => {
      // 初始化远程容器的共享作用域
      await __webpack_init_sharing__('default')
      const container = window[scope]
      await container.init(__webpack_share_scopes__.default)
      const factory = await window[scope].get(module)
      return factory()
    })
  }, [ready, scope, module])

  if (!Component) return <div>加载中...</div>
  return (
    <Suspense fallback={<div>加载组件中...</div>}>
      <Component />
    </Suspense>
  )
}

// 使用
function App() {
  return (
    <DynamicRemoteComponent
      url="http://localhost:3002/remoteEntry.js"
      scope="dashboardApp"
      module="./Dashboard"
    />
  )
}

一个完整的双应用示例

下面给出一个更贴近实际的项目结构。假设有一个主框架应用(Shell)和一个业务子应用(Dashboard):

// dashboard/webpack.config.js (Remote)
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  entry: './src/index.js',
  output: { publicPath: 'auto' },
  devServer: { port: 3002 },
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './Dashboard': './src/Dashboard',
        './Charts': './src/components/Charts',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        recharts: { singleton: true },
      },
    }),
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
}
// shell/webpack.config.js (Host)
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  entry: './src/index.js',
  output: { publicPath: 'auto' },
  devServer: { port: 3000 },
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        dashboard: 'dashboard@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
}

与其他微前端方案的对比

方案 接入成本 技术栈限制 沙箱隔离 适用场景
Module Federation Webpack 项目 无(共享运行时) 同技术栈团队协作
qiankun (umi) 无限制 JS 沙箱 + CSS 隔离 异构技术栈整合
iframe 极低 无限制 完全隔离 完全独立的子系统
Web Components 无限制 Shadow DOM 跨框架组件复用

注意事项与最佳实践

小结

Webpack Module Federation 以最小的侵入性实现了微前端的核心诉求:独立开发、独立部署、运行时集成。相比 iframe 和 JS 沙箱方案,它更轻量、性能更好,但代价是缺乏隔离性。如果你的团队技术栈统一(都是 Webpack + React 或 Webpack + Vue),模块联邦是最推荐的微前端方案。对于异构技术栈混合的场景,可以考虑 qiankun 等提供完整沙箱的框架。