微前端 Module Federation 2.0 实战:跨团队协作的终极方案

2920 字
15 分钟
微前端 Module Federation 2.0 实战:跨团队协作的终极方案

前言#

在大型前端项目中,多团队协作开发是一个绕不开的话题。传统的 monorepo 或 npm 包方案都有各自的痛点——monorepo 构建慢、耦合重;npm 包发版流程长、版本同步难。Webpack 5 带来的 Module Federation(模块联邦) 彻底改变了这个局面,而随着 2.0 版本的演进,它已经成为微前端架构中最成熟的运行时集成方案。

本文将从原理到实战,全面剖析 Module Federation 2.0 的核心机制,包括共享依赖、版本协商、动态远程加载,以及如何与 Vite 生态配合使用。

一、Module Federation 核心原理#

1.1 什么是模块联邦#

Module Federation 的核心思想很简单:让一个 JavaScript 应用在运行时动态加载另一个应用导出的模块,就像加载本地模块一样。

它引入了几个关键概念:

  • Host(宿主):消费远程模块的应用
  • Remote(远程):暴露模块供其他应用使用的应用
  • Shared(共享):多个应用之间共享的依赖(如 Vue、lodash)
  • Container(容器):每个参与联邦的应用都是一个容器,既可以是 Host 也可以是 Remote

1.2 底层加载机制#

Module Federation 的运行时核心是一个异步的模块加载协议。当 Host 需要加载 Remote 的模块时,大致经历以下过程:

1. Host 加载 Remote 的 remoteEntry.js(容器入口)
2. remoteEntry.js 注册自身到全局的 __webpack_share_scopes__
3. Host 调用 container.init(shareScope) 初始化共享作用域
4. Host 调用 container.get('./module') 获取远程模块
5. 远程模块的 chunk 被异步加载并执行
6. 返回模块导出,Host 正常使用

这个过程的关键代码在 Webpack 运行时中:

// Webpack 生成的运行时代码(简化版)
__webpack_require__.l = (url, done) => {
// 创建 script 标签加载远程入口
const script = document.createElement('script');
script.src = url;
script.onload = done;
document.head.appendChild(script);
};
// 初始化远程容器
const initRemote = async (remoteName, shareScope) => {
const container = window[remoteName];
if (!container.__initialized) {
await container.init(shareScope);
container.__initialized = true;
}
return container;
};
// 获取远程模块
const getModule = async (remoteName, modulePath) => {
const container = await initRemote(remoteName, __webpack_share_scopes__.default);
const factory = await container.get(modulePath);
return factory();
};

1.3 共享作用域(Share Scope)#

共享作用域是 Module Federation 最精妙的设计。它解决了一个核心问题:多个独立构建的应用如何共享同一份依赖,避免重复加载?

// webpack.config.js - Host 配置
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
shared: {
vue: {
singleton: true, // 全局只加载一个版本
requiredVersion: '^3.4.0',
eager: false, // 异步加载
},
'vue-router': {
singleton: true,
requiredVersion: '^4.3.0',
},
pinia: {
singleton: true,
requiredVersion: '^2.1.0',
},
},
}),
],
};

二、版本协商机制深入#

2.1 版本协商的工作流程#

当多个应用声明了同一个共享依赖但版本不同时,Module Federation 的版本协商机制会介入:

Host 声明: vue@^3.4.0
Remote A 声明: vue@^3.3.0
Remote B 声明: vue@^3.5.0

协商规则如下:

  1. singleton 模式:只加载一个版本,选择满足所有 requiredVersion 约束的最高版本
  2. 非 singleton 模式:每个应用可以加载自己需要的版本(会导致重复加载)
  3. strictVersion:如果版本不满足约束,抛出警告或错误
// 版本协商的运行时伪代码
const resolveSharedVersion = (shareScope, packageName) => {
const versions = shareScope[packageName];
// versions 示例:
// {
// '3.4.21': { get: () => ..., from: 'host', eager: false },
// '3.3.8': { get: () => ..., from: 'app1', eager: false },
// '3.5.1': { get: () => ..., from: 'app2', eager: false },
// }
if (config.singleton) {
// 找到满足所有 requiredVersion 的最高版本
const sorted = Object.keys(versions).sort(semver.rcompare);
for (const version of sorted) {
if (allConstraintsSatisfied(version, constraints)) {
return versions[version];
}
}
// 没有完全满足的,用最高版本并发出警告
console.warn(`Unsatisfied version ${sorted[0]} for shared ${packageName}`);
return versions[sorted[0]];
}
// 非 singleton: 返回满足当前应用约束的最佳版本
return findBestMatch(versions, currentAppConstraint);
};

2.2 常见版本冲突及解决方案#

// 场景:Remote 需要 vue@3.5,但 Host 只有 vue@3.4
// 解决方案 1: 放宽版本约束
shared: {
vue: {
singleton: true,
requiredVersion: '^3.3.0', // 放宽到 3.3+
},
}
// 解决方案 2: 允许 fallback
shared: {
vue: {
singleton: true,
requiredVersion: '^3.5.0',
strictVersion: false, // 不满足时不报错,降级使用可用版本
},
}
// 解决方案 3: eager 加载确保版本优先
shared: {
vue: {
singleton: true,
requiredVersion: '^3.5.0',
eager: true, // 在入口 chunk 中直接打包,确保此版本优先被注册
},
}

三、动态远程加载#

3.1 静态配置的局限#

静态配置要求在构建时就确定所有 Remote 的地址:

// 静态配置 - 构建时确定
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
}

这在实际生产环境中很不灵活——Remote 的地址可能根据环境不同而变化,甚至某些 Remote 可能需要按需加载。

3.2 运行时动态加载#

Module Federation 2.0 提供了更强大的动态加载能力:

webpack.config.js
// 方案一:使用 promise 形式的 remote
remotes: {
app1: `promise new Promise((resolve) => {
const remoteUrl = window.__REMOTE_CONFIG__.app1 + '/remoteEntry.js';
const script = document.createElement('script');
script.src = remoteUrl;
script.onload = () => {
resolve({
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg);
} catch (e) {
console.log('Remote app1 already initialized');
}
},
});
};
document.head.appendChild(script);
})`,
}
// 方案二:封装通用的动态远程加载器
class DynamicRemoteLoader {
constructor() {
this.remoteCache = new Map();
this.loadingPromises = new Map();
}
/**
* 动态加载远程模块
* @param {string} remoteName - 远程应用名称
* @param {string} remoteUrl - remoteEntry.js 的 URL
* @param {string} modulePath - 模块路径,如 './Button'
*/
async loadModule(remoteName, remoteUrl, modulePath) {
// 确保远程容器已加载
const container = await this.loadRemoteContainer(remoteName, remoteUrl);
// 初始化共享作用域
await this.initContainer(container);
// 获取模块
const factory = await container.get(modulePath);
return factory();
}
async loadRemoteContainer(remoteName, remoteUrl) {
if (this.remoteCache.has(remoteName)) {
return this.remoteCache.get(remoteName);
}
// 防止重复加载
if (!this.loadingPromises.has(remoteName)) {
this.loadingPromises.set(remoteName, this.doLoadScript(remoteName, remoteUrl));
}
const container = await this.loadingPromises.get(remoteName);
this.remoteCache.set(remoteName, container);
this.loadingPromises.delete(remoteName);
return container;
}
doLoadScript(remoteName, url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
script.async = true;
script.onload = () => {
const container = window[remoteName];
if (container) {
resolve(container);
} else {
reject(new Error(`Remote container ${remoteName} not found on window`));
}
};
script.onerror = (err) => {
reject(new Error(`Failed to load remote ${remoteName} from ${url}`));
};
document.head.appendChild(script);
});
}
async initContainer(container) {
if (container.__initialized) return;
// 获取 webpack 的共享作用域
// @ts-ignore
const shareScope = __webpack_share_scopes__;
if (!shareScope.default) {
// @ts-ignore
await __webpack_init_sharing__('default');
}
await container.init(shareScope.default);
container.__initialized = true;
}
}
// 使用示例
const loader = new DynamicRemoteLoader();
// 根据配置中心动态加载
const config = await fetch('/api/micro-app-config').then(r => r.json());
// config = { name: 'dashboard', url: 'https://cdn.example.com/dashboard/remoteEntry.js' }
const DashboardModule = await loader.loadModule(
config.name,
config.url,
'./DashboardWidget'
);

3.3 配合 Vue 的异步组件#

在 Vue 项目中,可以将远程模块与异步组件完美结合:

remoteComponents.js
import { defineAsyncComponent, h } from 'vue';
const loader = new DynamicRemoteLoader();
export function createRemoteComponent(remoteName, remoteUrl, modulePath) {
return defineAsyncComponent({
loader: () => loader.loadModule(remoteName, remoteUrl, modulePath),
loadingComponent: {
render() {
return h('div', { class: 'remote-loading' }, '加载中...');
},
},
errorComponent: {
props: ['error'],
render() {
return h('div', { class: 'remote-error' }, `加载失败: ${this.error?.message}`);
},
},
delay: 200,
timeout: 10000,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
console.warn(`Remote component load failed, retrying (${attempts}/3)...`);
retry();
} else {
fail();
}
},
});
}
// 在路由或模板中使用
const RemoteHeader = createRemoteComponent(
'headerApp',
'https://cdn.example.com/header/remoteEntry.js',
'./Header'
);

四、Module Federation 2.0 新特性#

4.1 @module-federation/enhanced#

MF 2.0 通过 @module-federation/enhanced 包提供了大量增强能力:

Terminal window
npm install @module-federation/enhanced
// webpack.config.js - MF 2.0 增强配置
const { ModuleFederationPlugin } = require('@module-federation/enhanced');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
remote_app: 'remote_app@http://localhost:3001/mf-manifest.json',
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
},
// 2.0 新增:运行时插件
runtimePlugins: [
require.resolve('./mf-runtime-plugin'),
],
}),
],
};

4.2 运行时插件系统#

MF 2.0 最强大的特性之一是运行时插件系统,允许在模块加载的各个阶段注入自定义逻辑:

mf-runtime-plugin.js
const runtimePlugin = () => ({
name: 'custom-mf-plugin',
// 在加载远程入口前触发
beforeInit(args) {
console.log('Initializing federation:', args.options.name);
return args;
},
// 在初始化共享作用域时触发
init(args) {
// 可以修改共享配置
return args;
},
// 在解析远程地址时触发 - 实现动态路由
async resolveRemote(args) {
// 从配置中心获取真实地址
if (args.id === 'remote_app') {
const config = await fetch('/api/remote-config').then(r => r.json());
args.url = config.remoteAppUrl;
}
return args;
},
// 加载远程模块后触发
afterResolve(args) {
console.log(`Loaded module: ${args.id}`);
return args;
},
// 错误处理
errorLoadRemote(args) {
console.error(`Failed to load remote: ${args.id}`, args.error);
// 返回 fallback 模块
if (args.id === 'remote_app/Widget') {
return {
default: () => ({ template: '<div>远程组件不可用</div>' }),
};
}
return args;
},
});
export default runtimePlugin;

4.3 Manifest 协议#

MF 2.0 引入了 manifest 协议,替代了传统的 remoteEntry.js:

// mf-manifest.json(自动生成)
{
"id": "remote_app",
"name": "remote_app",
"metaData": {
"name": "remote_app",
"buildInfo": {
"buildVersion": "1.0.0",
"buildTime": "2026-04-05T10:00:00Z"
}
},
"shared": [
{
"assets": { "js": { "sync": ["shared-vue.js"], "async": [] } },
"sharedName": "vue",
"version": "3.4.21"
}
],
"exposes": [
{
"path": "./Widget",
"assets": {
"js": { "sync": ["expose-Widget.js"], "async": ["chunk-abc123.js"] }
}
}
]
}

Manifest 的优势在于:Host 可以精确知道 Remote 暴露了哪些模块、使用了哪些共享依赖版本,从而实现更智能的预加载和版本协商。

五、配合 Vite 使用#

5.1 @module-federation/vite#

Vite 生态中,可以通过 @module-federation/vite 插件使用 Module Federation:

Terminal window
npm install @module-federation/vite
// vite.config.ts - Remote 端
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
vue(),
federation({
name: 'remote_app',
filename: 'remoteEntry.js',
exposes: {
'./Widget': './src/components/Widget.vue',
'./utils': './src/utils/index.ts',
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
pinia: { singleton: true, requiredVersion: '^2.1.0' },
},
}),
],
build: {
target: 'esnext',
minify: false, // 开发阶段关闭压缩便于调试
},
});
// vite.config.ts - Host 端
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
vue(),
federation({
name: 'host_app',
remotes: {
remote_app: {
type: 'module',
name: 'remote_app',
entry: 'http://localhost:5001/remoteEntry.js',
entryGlobalName: 'remote_app',
},
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
pinia: { singleton: true, requiredVersion: '^2.1.0' },
},
}),
],
});

5.2 Vite + MF 的开发体验优化#

// src/bootstrap.ts - 异步引导(重要!)
// MF 的共享依赖需要异步初始化,因此入口必须是异步的
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
// src/main.ts - 真正的入口
// 通过动态 import 实现异步引导
import('./bootstrap');
src/views/Dashboard.vue
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
// 直接像本地模块一样导入远程组件
const RemoteWidget = defineAsyncComponent(
() => import('remote_app/Widget')
);
// 也可以导入远程工具函数
const { formatDate } = await import('remote_app/utils');
const now = ref(formatDate(new Date()));
</script>
<template>
<div class="dashboard">
<h1>Host 应用 Dashboard</h1>
<p>当前时间: {{ now }}</p>
<Suspense>
<RemoteWidget />
<template #fallback>
<div>远程组件加载中...</div>
</template>
</Suspense>
</div>
</template>

六、生产环境最佳实践#

6.1 错误边界与降级策略#

src/composables/useRemoteModule.ts
import { ref, shallowRef, onMounted } from 'vue';
interface UseRemoteModuleOptions {
remoteName: string;
modulePath: string;
fallback?: any;
retries?: number;
timeout?: number;
}
export function useRemoteModule<T = any>(options: UseRemoteModuleOptions) {
const { remoteName, modulePath, fallback = null, retries = 3, timeout = 10000 } = options;
const module = shallowRef<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(true);
const loadWithRetry = async (attempt = 1): Promise<T> => {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const mod = await import(/* @vite-ignore */ `${remoteName}/${modulePath}`);
clearTimeout(timer);
return mod;
} catch (err) {
if (attempt < retries) {
// 指数退避
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 500));
return loadWithRetry(attempt + 1);
}
throw err;
}
};
onMounted(async () => {
try {
module.value = await loadWithRetry();
} catch (err) {
error.value = err as Error;
module.value = fallback;
console.error(`Failed to load remote module ${remoteName}/${modulePath}:`, err);
} finally {
loading.value = false;
}
});
return { module, error, loading };
}

6.2 部署策略#

# nginx.conf - Remote 应用的 CORS 和缓存配置
server {
listen 80;
server_name remote-app.example.com;
location / {
root /usr/share/nginx/html;
# CORS - 允许 Host 跨域加载
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
# remoteEntry 不缓存,确保 Host 总是获取最新版本
location ~* remoteEntry\.js$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Access-Control-Allow-Origin *;
}
# mf-manifest.json 同样不缓存
location ~* mf-manifest\.json$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Access-Control-Allow-Origin *;
}
# 其他静态资源长期缓存(文件名含 hash)
location ~* \.(js|css|png|jpg|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin *;
}
}
}

七、总结#

Module Federation 2.0 是微前端架构的重大突破。它的核心优势在于:

  1. 运行时集成:无需重新构建 Host,Remote 独立部署即可生效
  2. 智能共享:版本协商机制避免依赖重复加载
  3. 灵活扩展:运行时插件系统支持自定义加载逻辑
  4. 生态兼容:Webpack 和 Vite 双生态支持

在选择微前端方案时,如果你的场景是多团队独立开发、独立部署、运行时集成,Module Federation 2.0 几乎是目前最优解。配合良好的错误处理和降级策略,它完全可以支撑大规模生产环境。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

微前端 Module Federation 2.0 实战:跨团队协作的终极方案
https://boke.hackerdream.xyz/posts/micro-frontend-module-federation/
作者
晴天
发布于
2026-03-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录