微前端实战:Module Federation 与 Single-SPA

FreeGuideOnline 最新 2026-06-12

微前端架构:从概念到落地,用 Module Federation 与 Single-SPA 构建企业级应用

引言:为什么需要微前端

当单体前端项目膨胀到一定程度,构建时间变长、协作冲突频繁、技术栈锁定——这正是微前端试图解决的问题。它借鉴了微服务的思想,将前端应用拆分为多个更小、更松散耦合的“微应用”,每个微应用可独立开发、测试、部署,最终在主应用中无缝聚合。

本教程面向对微前端感兴趣的初中级开发者,通过 Module FederationSingle-SPA 的实战演示,帮助你从理解核心原理到动手搭建一个可运行的微前端架构。

第一部分:微前端核心概念

在动手之前,先建立统一认知。

什么是微前端

  • 技术定义:一种将 Web 界面拆分为独立、可由不同团队端到端负责的应用单元,并组合成一个统一产品的架构风格。
  • 核心价值
    • 独立部署:每个微应用有独立的 CI/CD 管道。
    • 技术栈无关:允许 Angular、React、Vue 等不同框架共存。
    • 团队自治:各团队可按自己的节奏迭代。
  • 与传统 SPA 的对比
    • 单体 SPA:单个仓库、统一构建、单一发布。
    • 微前端:多仓库、独立构建、独立发布,运行时组合。

关键设计原则

  1. 技术不可知:主框架不应限制子应用使用的技术栈。
  2. 隔离性:JavaScript 和 CSS 需作用域隔离,避免冲突。
  3. 通信标准:通过明确的接口或事件总线进行跨应用通讯。
  4. 渐进式提升:可从简单外壳逐步迁移现有项目。

第二部分:技术选型与方案对比

实现微前端的方式有很多,这里聚焦业界两大主流方案,并直接对比其优势与适用场景。

特性 Module Federation (Webpack 5) Single-SPA
工作方式 运行时动态加载远程模块,构建时确定依赖关系 路由驱动,加载并挂载/卸载整个应用的生命周期
共享依赖 原生支持,可配置共享库防重复加载 需自行处理(或在根配置中手动引入公共依赖)
技术栈混合 更擅长在同一框架不同版本间共享,跨框架需额外封装 天然支持任意框架,因为每个微应用独立运行
入门复杂度 中等,需理解 webpack 配置 较低,API 简洁,文档齐全
最佳场景 同一技术栈(如都是 React),需极致共享模块和依赖 需集成不同框架的遗留系统,或严格按子域拆分

本教程采用组合方案:用 Single-SPA 作为整个微前端的组织者(根配置),负责路由分发和应用生命周期管理;用 Module Federation 实现微应用之间的模块动态共享与加载。这样兼顾了路由灵活性和资源复用能力。

第三部分:环境搭建与目录结构

开始实战前,确保环境:

  • Node.js >= 16
  • npm 或 yarn
  • 现代浏览器

我们将创建三个子项目:

  • root-config:Single-SPA 根配置,负责路由分发。
  • app1:一个 React 微应用,通过 Module Federation 暴露组件。
  • app2:一个 Vue 3 微应用,消费 app1 暴露的组件,同时也被根配置加载。

最终目录结构:

micro-frontend-demo/
├── root-config/               # Single-SPA 根配置
│   ├── src/
│   │   └── index.ejs
│   │   └── microfrontend-layout.html
│   ├── webpack.config.js
│   └── package.json
├── app1/                      # React 微应用(提供商)
│   ├── src/
│   ├── webpack.config.js
│   └── package.json
└── app2/                      # Vue 3 微应用(消费者 + 独立应用)
    ├── src/
    ├── webpack.config.js
    └── package.json

第四部分:构建 React 微应用(app1)—— 使用 Module Federation 暴露模块

初始化项目与依赖

mkdir app1 && cd app1
npm init -y
npm install react react-dom react-router-dom single-spa-react
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin @babel/core babel-loader @babel/preset-react

webpack 配置:启用 Module Federation

创建 webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: { port: 3001 },
  output: { publicPath: 'auto' },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react'] } }
      }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',      // 对外暴露的入口文件
      exposes: {
        './Header': './src/Header',    // 暴露一个 React 组件
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true },
      },
    }),
    new HtmlWebpackPlugin({ template: './public/index.html' })
  ],
};

组件与入口

创建 src/Header.js

import React from 'react';

const Header = ({ title }) => {
  return <header style={{ background: '#6200ee', color: 'white', padding: '10px' }}>
    <h2>{title || 'Micro App1 Header'}</h2>
  </header>;
};
export default Header;

创建 src/App.js

import React from 'react';
import { BrowserRouter, Link } from 'react-router-dom';
import Header from './Header';

const App = () => (
  <BrowserRouter>
    <Header title="App1" />
    <p>This is App1 content.</p>
  </BrowserRouter>
);

// Single-SPA 需要 lifecycle 导出
export const { bootstrap, mount, unmount } = singleSpaReact({
  React,
  ReactDOM: require('react-dom'),
  rootComponent: App,
  errorBoundary: (err) => <div>Error</div>,
});

注意:Single-SPA 生命周期集成稍后会在打包时一并处理,此处暂简化,实际可使用 single-spa-react 帮助工具。

将应用作为 Single-SPA 的 parcel 运行,但也可以独立启动。为演示 MF 暴露,我们直接通过开发服务器提供 remoteEntry。

启动 npm start,此后 http://localhost:3001/remoteEntry.js 即可访问暴露的模块。

第五部分:构建 Vue 微应用(app2)—— 消费远程模块,并注册到 Single-SPA

项目初始化

mkdir app2 && cd app2
npm init -y
npm install vue@3 vue-router@4 single-spa-vue
npm install -D webpack webpack-cli webpack-dev-server vue-loader@next @vue/compiler-sfc

webpack 配置:作为消费者加载远程模块

const { ModuleFederationPlugin } = require('webpack').container;
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  entry: './src/main.js',
  mode: 'development',
  devServer: { port: 3002 },
  output: { publicPath: 'auto' },
  resolve: { extensions: ['.js', '.vue'] },
  module: {
    rules: [
      { test: /\.vue$/, loader: 'vue-loader' },
      { test: /\.js$/, loader: 'babel-loader' }
    ]
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      remotes: {
        // 远程模块名称: 格式为「名称@地址/remoteEntry.js」
        app1: 'app1@http://localhost:3001/remoteEntry.js',
      },
      shared: {} // 可共享 vue,但为了避免版本冲突先用空
    }),
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({ template: './public/index.html' })
  ],
};

在 Vue 组件中使用远程的 Header

创建 src/App.vue

<template>
  <div>
    <RemoteHeader :title="dynamicTitle" />
    <p>App2 (Vue) consuming React component!</p>
  </div>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue';

export default {
  components: {
    RemoteHeader: defineAsyncComponent(() => import('app1/Header')),
  },
  setup() {
    const dynamicTitle = ref('Header rendered by Vue');
    return { dynamicTitle };
  }
};
</script>

远程模块通过 import('app1/Header') 动态加载,Vue 的 defineAsyncComponent 将其包装为异步组件。

Single-SPA 生命周期集成

src/main.js 中,使用 single-spa-vue 创建生命周期:

import { createApp, h } from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';

const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render() { return h(App); }
  },
  // 可传递 VueRouter 实例等
});

export const { bootstrap, mount, unmount } = vueLifecycles;

为了让 Single-SPA 找到这个应用,我们需要将入口打包为 SystemJS 格式?实际上 Single-SPA 推荐微应用打包成 SystemJS 或直接使用 esm。这里我们适配 Module Federation 的异步加载,根配置会以动态 import 方式加载 app2 的入口。所以需配置 output.libraryTarget: 'system'?Module Federation 产生异步块,在 Single-SPA 上下文中可直接使用动态导入。

更简单的方法:根配置中使用 System.import 或直接 import()。我们让 app2 构建时产出 main.js 作为入口,并在根配置通过 url 加载。

修改 webpack output:

output: {
  // ... 其余不变
  library: { type: 'system' },   // 生成 System.register 模块供 Single-SPA 使用
  publicPath: 'auto',
}

然后需要新增配置使微应用的 chunk 能被正确加载。但 Module Federation 与 System 格式混合可能带来复杂度。这里推荐使用 Single-SPA 官方的 @single-spa/welcome 插件?不,我们采用另一种干净方案:

替代方案:使用 single-spa 本身的 import-maps 或直接 import()。Module Federation 的 remote 加载是异步的,可以直接在根配置中写 import('app2@http://localhost:3002/main.js')。通过将 app2 的入口配置成输出一个包含 lifecycle 的模块即可,无需 system 格式。设置:

output: {
  filename: 'main.js',
  library: { type: 'module' },   // 实验性,需要 script type module
  publicPath: 'auto',
},
experiments: { outputModule: true },

浏览器支持模块类型后,可以直接 import。但为了兼容,我们使用 Single-SPA 推荐的 "in-browser module" 方式:打包成 ES module 或 System。这里为简化,我们沿用 System 格式,并关闭 Module Federation 的 shared scope 冲突(使用 library.type: 'system' 确实会与 Module Federation 的动态加载冲突,因为 System 格式不能同时使用异步块)。因此调研发现:Module Federation 和 System 格式不能直接混用

因此,我们调整策略:app2 不采用自身作为一个完整的 Single-SPA 应用,而是作为独立的微应用,通过 Module Federation 加载 app1 的组件,但在根配置 app2 的入口文件依然需要暴露 lifecycle。我们可以将 Vue 应用单独打包,但不使用 System 格式,而是将生命周期导出为一个全局变量,并让根配置通过动态 script 加载后调用。但更现实的做法是:根配置(root-config)也使用 Module Federation,这样所有应用都通过 MF 连接,而 Single-SPA 只作为路由框架。

这就是常见的 “MF + Single-SPA” 组合:根配置本身也是一个 webpack 项目,配置 Module Federation 的 remotes 指向所有子应用,然后每个子应用暴露入口(bootstrap/mount/unmount),根配置通过 import 动态加载这些入口并调用 Single-SPA 注册。这样所有子应用都无需 System 构建,直接使用标准 ES 模块,并且共享依赖可以由 MF 管理。

下面我们就按此模式完成 root-config。

第六部分:根配置(root-config)—— Single-SPA 组织者

创建根配置项目

mkdir root-config && cd root-config
npm init -y
npm install single-spa
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin

webpack 配置:作为远程消费者的容器

const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: { port: 9000, historyApiFallback: true },
  output: { publicPath: '/' },
  plugins: [
    new ModuleFederationPlugin({
      name: 'root',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js',
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      shared: {},
    }),
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
};

注意,app2 现在也需要一个 remoteEntry.js 来暴露自己的生命周期,所以改造 app2 的 webpack 配置,增加 exposes

exposes: {
  './App2Lifecycle': './src/main',   // 暴露生命周期对象
}

相应地,app2 的 main.js 导出 bootstrap, mount, unmount

根配置的 Single-SPA 注册与路由

src/index.js:

import { registerApplication, start } from 'single-spa';

// 动态导入生命周期函数
registerApplication({
  name: '@org/app1',
  app: () => import('app1/App1Lifecycle'), // app1 也需暴露 lifecycle
  activeWhen: ['/app1']
});

registerApplication({
  name: '@org/app2',
  app: () => import('app2/App2Lifecycle'),
  activeWhen: ['/app2']
});

start({
  urlRerouteOnly: true,
});

app1 同样需要在自己项目里增加 exposes: { './App1Lifecycle': './src/root.component' } 并导出生命周期。

现在所有应用都通过 Module Federation 连接,根配置负责路由。

共享依赖优化

在 MF 配置中合理使用 shared 避免加载多次 React、Vue。在 app1 中 shared 配置 react 和 react-dom;app2 中 shared vue;根配置可共享 routing 相关库。

第七部分:实践中的常见问题与解决方案

1. 样式隔离

  • 使用 CSS Modules 或 CSS-in-JS。
  • 通过 Shadow DOM 封装(Single-SPA 提供 mountParcel 模式可配合)。
  • 命名约定:给每个微应用的根元素添加唯一类名前缀。

2. 跨应用通信

  • Props 传递:通过 Single-SPA 的 mount 函数传递自定义 props。
  • Custom Eventswindow.dispatchEvent 发射全局事件,其他微应用监听。
  • 共享 Store:不推荐直接共享状态,但可通过共享的事件总线或独立的状态管理模块(如 Redux 动态注册)实现。
  • Module Federation 共享模块:可以将共用的工具函数、API 客户端作为 MF 的 shared 模块。

3. 性能优化

  • 预加载远程入口:在根配置中提前触发 import('app1/..')
  • 设置共享依赖的 eager: true 避免请求瀑布流。
  • 使用 HTTP/2 和多路复用加速远程模块下载。

4. 版本兼容与降级

  • 使用 requiredVersion 限制共享库版本。
  • shared 中配置 strictVersion: false 允许不匹配时的回退。
  • 通过 singleton: true 保证只加载一个实例。

第八部分:生产环境部署考量

  • 独立构建与持续部署:每个微应用单独构建并上传到 CDN,remoteEntry.js 地址指向生产 CDN。
  • 环境变量管理:通过 Webpack 的 DefinePlugin 为每个微应用注入不同 API 地址。
  • 异常隔离:实现 error boundary,防止单个应用崩溃影响整个页面。
  • 监控:集成 Sentry 等错误收集,并标记是哪个微应用报错。
  • 缓存策略remoteEntry.js 设置短期缓存或不缓存,确保版本更新实时;子应用 chunk 使用 contenthash 文件名长期缓存。

总结

通过本教程,你已掌握使用 Module Federation 和 Single-SPA 构建微前端架构的完整步骤:从单一组件暴露、跨技术栈消费,到根配置的路由组织。这套组合充分发挥了 MF 的模块共享优势和 Single-SPA 的路由灵活性。

下一步建议:动手实现一个包含三个不同技术栈微应用的示例工程,并尝试加入共享状态管理,逐步深化对微前端工程化的理解。记住,微前端不是银弹,它带来独立部署便利的同时也引入了运行时复杂度,始终以团队和项目的实际需求为出发点进行选型。