Webpack 5 的 Module Federation
#webpack
目录
- 1. 总结
- 2. Module Federation 基本概念
- 3. 使用配置说明
- 4. 示例 1:主应用和远程组件
- 5. 示例 2:动态远程加载示例
- 6. 示例 3:共享状态管理
- 7. 示例 4:路由集成示例(React Router)
- 8. 示例 5:带版本控制的共享依赖示例
- 9. 示例 6:错误边界
- 10. 示例 7:错误边界
- 11. 示例 8:带认证的模块联邦示例
- 12. 常见问题
1. 总结
- Module Federation 的特点,两个
- ==动态加载==另一个应用的代码和依赖
- 可在运行时==共享代码==
- 配置说明:
- new ModuleFederationPlugin
- remotes
- exposes
- shared:
- 比如 React
- 可指定 requiredVersion 版本
- singleton 是否单例
- 比如 React
- new ModuleFederationPlugin
- 示例:
- 使用 promise ==可自定义动态远程加载==
- 错误边界,自己写代码,可以自己实现
- 请求时可添加==认证逻辑==
- 共享状态管理,记得 exposes
- 使用 promise ==可自定义动态远程加载==
- 其他
remoteEntry.js
是 Module Federation 中的一个==约定俗成==的命名- 也可
- ① 根据环境使用不同的文件名
- ② 添加 hash 以处理缓存
- ③ 用版本号作为文件名的一部分
- 也可
- 端口配置有什么要求
- 建议==不同应用分配不同端口==,比如主应用、用户管理管理
[远程应用名称]@[远程入口文件URL]
app1@http://localhost:3001/remoteEntry.js
关于 vite 如何使用模块联邦,可见 11. vite 中如何使用 Module Federation
2. Module Federation 基本概念
- Module Federation 允许一个 JavaScript 应用动态地加载另一个应用的代码和依赖。
- 它是实现微前端的一种方式,使得不同的构建可以在运行时共享代码。
3. 使用配置说明
3.1. 主应用配置
// webpack.config.js - 主应用配置示例
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host', // 当前应用名称
filename: 'remoteEntry.js', // 暴露的文件名称
remotes: { // 声明需要使用的远程应用
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
// 暴露给其他应用的模块
exposes: {
'./Header': './src/components/Header',
'./Footer': './src/components/Footer'
},
// 共享依赖
shared: {
react: {
singleton: true, // 确保只加载一个版本
requiredVersion: '^17.0.2'
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.2'
}
}
})
]
};
3.2. 子应用配置
// webpack.config.js - 子应用配置示例
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Card': './src/components/Card'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
3.3. 使用示例:主应用中加载远程组件
// 主应用中加载远程组件
// App.js
import React, { Suspense } from 'react';
// 使用动态导入加载远程组件
// 需要子应用配置,并且expose
const RemoteButton = React.lazy(() => import('app1/Button'));
const RemoteCard = React.lazy(() => import('app1/Card'));
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback="Loading Button...">
<RemoteButton />
</Suspense>
<Suspense fallback="Loading Card...">
<RemoteCard />
</Suspense>
</div>
);
}
// 入口文件 index.js
import('./bootstrap').catch(err => console.error(err));
4. 示例 1:主应用和远程组件
4.1. 主应用:3001 端口
// webpack.config.js (主应用)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
entry: "./src/index",
mode: "development",
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
remote: "remote@http://localhost:3001/remoteEntry.js",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true }
}
}),
],
};
// src/App.js (主应用)
import React, { Suspense } from "react";
const RemoteButton = React.lazy(() => import("remote/Button"));
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback="Loading Button...">
<RemoteButton />
</Suspense>
</div>
);
}
4.2. Remote Application (远程应用) 的 Button 组件,需要 exposes
// webpack.config.js (远程应用)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
entry: "./src/index",
mode: "development",
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: "remote",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/Button",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true }
}
}),
],
};
// src/Button.js (远程应用) 组件
import React from "react";
const Button = () => (
<button onClick={() => alert("Hello from Remote!")}>
Remote Button
</button>
);
export default Button;
5. 示例 2:动态远程加载示例
5.1. 主应用
// webpack.config.js (主应用)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
// 动态远程配置
remote: `promise new Promise(resolve => {
const remoteUrl = window.location.hostname === 'localhost'
? 'http://localhost:3001'
: 'https://production.com';
const script = document.createElement('script');
script.src = '${remoteUrl}/remoteEntry.js';
script.onload = () => {
resolve(window.remote);
};
document.head.appendChild(script);
})`,
},
shared: { react: { singleton: true } }
}),
],
};
// App.js
const loadComponent = async () => {
const component = await import('remote/Component');
return component.default;
};
6. 示例 3:共享状态管理
/// store-app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'store',
filename: 'remoteEntry.js',
exposes: {
'./store': './src/store',
'./StoreProvider': './src/StoreProvider'
},
shared: {
react: { singleton: true },
'react-redux': { singleton: true },
'@reduxjs/toolkit': { singleton: true }
}
})
]
};
// consumer-app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'consumer',
remotes: {
store: 'store@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-redux': { singleton: true },
'@reduxjs/toolkit': { singleton: true }
}
})
]
};
// store-app/src/store.js
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
// reducers
}
});
// consumer-app/src/App.js
import { Provider } from 'react-redux';
import { store } from 'store/store';
export default function App() {
return (
<Provider store={store}>
{/* 应用内容 */}
</Provider>
);
}
7. 示例 4:路由集成示例(React Router)
- 多个远程应用
// webpack.config.js (主应用)
new ModuleFederationPlugin({
name: "host",
remotes: {
remote1: "remote1@http://localhost:3001/remoteEntry.js",
remote2: "remote2@http://localhost:3002/remoteEntry.js",
},
shared: {
react: { singleton: true },
"react-router-dom": { singleton: true },
},
});
// App.js (主应用)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const RemoteApp1 = React.lazy(() => import("remote1/App"));
const RemoteApp2 = React.lazy(() => import("remote2/App"));
function App() {
return (
<BrowserRouter>
<Suspense fallback="Loading...">
<Routes>
<Route path="/app1/*" element={<RemoteApp1 />} />
<Route path="/app2/*" element={<RemoteApp2 />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
8. 示例 5:带版本控制的共享依赖示例
// webpack.config.js
new ModuleFederationPlugin({
name: "host",
remotes: {
remote: "remote@http://localhost:3001/remoteEntry.js",
},
shared: {
react: {
singleton: true,
requiredVersion: "^18.0.0",
},
"react-dom": {
singleton: true,
requiredVersion: "^18.0.0",
},
"@material-ui/core": {
singleton: true,
requiredVersion: "^4.12.0",
},
},
});
9. 示例 6:错误边界
// 主应用配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: `promise new Promise((resolve, reject) => {
const remoteUrl = 'http://localhost:3001/remoteEntry.js';
const script = document.createElement('script');
script.src = remoteUrl;
script.onerror = () => {
console.error('Failed to load remote app');
resolve({
get: (request) => {
return Promise.resolve(() => {
return () => {
return <div>Failed to load remote component</div>;
};
});
},
init: () => {}
});
};
script.onload = () => {
resolve(window.remoteApp);
};
document.head.appendChild(script);
})`
}
})
]
};
// 使用带错误处理的远程组件
const RemoteComponent = React.lazy(() => {
return import('remoteApp/Component')
.catch(err => {
console.error('Failed to load remote component:', err);
return { default: () => <div>Fallback UI</div> };
});
});
10. 示例 7:错误边界
// ErrorBoundary.js
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1>远程组件加载失败</h1>;
}
return this.props.children;
}
}
// App.js
const RemoteComponent = React.lazy(() => import("remote/Component"));
function App() {
return (
<ErrorBoundary>
<Suspense fallback="Loading...">
<RemoteComponent />
</Suspense>
</ErrorBoundary>
);
}
11. 示例 8:带认证的模块联邦示例
// webpack.config.js
new ModuleFederationPlugin({
name: "host",
remotes: {
remote: `promise new Promise(resolve => {
const script = document.createElement('script');
script.src = 'http://localhost:3001/remoteEntry.js';
// 添加认证token
script.crossOrigin = 'anonymous';
const token = localStorage.getItem('auth_token');
if (token) {
script.setAttribute('data-auth', token);
}
script.onload = () => {
resolve(window.remote);
};
document.head.appendChild(script);
})`,
},
});
12. 常见问题
12.1. 为什么都叫 remoteEntry.js ?
remoteEntry.js
是 Module Federation 中的一个约定俗成的命名,但这个名称实际上是可以自定义的,建议可以根据需要自定义入口文件名:
- 版本管理
- 缓存控制
- 环境区分
// ① 根据环境使用不同的文件名
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
// 根据环境使用不同的文件名
filename: process.env.NODE_ENV === 'production'
? 'remoteEntry.prod.js'
: 'remoteEntry.dev.js',
exposes: {
'./Component': './src/Component'
}
})
]
};
// ② 添加 hash 以处理缓存
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
// 添加 hash 以处理缓存
filename: 'remoteEntry.[contenthash].js',
exposes: {
'./Component': './src/Component'
}
})
]
};
// ③ 用版本号作为文件名的一部分
// webpack.config.js
const package = require('./package.json');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
// 使用版本号作为文件名的一部分
filename: `remoteEntry.${package.version}.js`,
exposes: {
'./Component': './src/Component'
}
})
]
};
12.2. 端口配置有什么要求?
建议提取成变量,如下代码
// ports.config.js
module.exports = {
host: 3000,
remotes: {
app1: 3001,
app2: 3002,
app3: 3003
}
};
// 示例端口分配方案
const ports = {
host: 3000, // 主应用
auth: 3001, // 认证应用
dashboard: 3002, // 仪表盘应用
users: 3003, // 用户管理应用
orders: 3004 // 订单管理应用
};
// 不同环境的端口范围示例
const portRanges = {
development: {
start: 3000,
end: 3999
},
testing: {
start: 4000,
end: 4999
},
staging: {
start: 5000,
end: 5999
}
};
12.3. app1@
?
- 作用
- 标识远程模块的来源
- 确保模块正确加载
- 维护模块间的依赖关系
- 规则:
app1
: 远程应用的名称@
: 分隔符http://localhost:3001/remoteEntry.js
: 远程入口文件的URL
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// 格式: [远程应用名称]@[远程入口文件URL]
app1: 'app1@http://localhost:3001/remoteEntry.js',
// 等价于
app1: `${remoteName}@${remoteUrl}`
}
})
]
};