微前端 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.0Remote A 声明: vue@^3.3.0Remote B 声明: vue@^3.5.0协商规则如下:
- singleton 模式:只加载一个版本,选择满足所有
requiredVersion约束的最高版本 - 非 singleton 模式:每个应用可以加载自己需要的版本(会导致重复加载)
- 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: 允许 fallbackshared: { 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 提供了更强大的动态加载能力:
// 方案一:使用 promise 形式的 remoteremotes: { 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 项目中,可以将远程模块与异步组件完美结合:
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 包提供了大量增强能力:
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 最强大的特性之一是运行时插件系统,允许在模块加载的各个阶段注入自定义逻辑:
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:
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');<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 错误边界与降级策略
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 是微前端架构的重大突破。它的核心优势在于:
- 运行时集成:无需重新构建 Host,Remote 独立部署即可生效
- 智能共享:版本协商机制避免依赖重复加载
- 灵活扩展:运行时插件系统支持自定义加载逻辑
- 生态兼容:Webpack 和 Vite 双生态支持
在选择微前端方案时,如果你的场景是多团队独立开发、独立部署、运行时集成,Module Federation 2.0 几乎是目前最优解。配合良好的错误处理和降级策略,它完全可以支撑大规模生产环境。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!