TL;DR
这是一篇用最小 demo 帮你理解Webpack Module Federation的文章。没有微前端术语,没有复杂部署,只展示最核心的一件事:如何在主应用中动态加载另一个项目里暴露出来的组件。
适合没接触过Module Federation的同学快速入门。最终的效果如下图所示:
模块联邦是什么
Webpack Module Federation(模块联邦)是从 Webpack 5 开始引入的一项功能,核心目的是解决多个独立应用之间共享模块和代码复用的问题。它的设计初衷是解决模块复用、依赖共享和多团队协作中的一些难题,是构建现代微前端架构的核心能力之一。
多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。
这通常被称作微前端,但并不仅限于此。
模块联邦可以做什么
Webpack Module Federation主要解决了以下几个前端开发中的实际问题:
1. 实现多个独立项目间的模块共享
- 问题:以前如果多个团队维护多个项目(如多个微前端),每个项目都要独立打包部署,难以共享组件库或逻辑模块。
- 解决方案:Module Federation 允许一个项目加载另一个项目暴露出来的模块,就像本地模块一样使用,而不需要重新打包这些模块。
2. 动态远程加载模块,支持异步更新
- 问题:传统的包共享通常依赖 npm 安装,更新依赖需要重新打包部署。
- 解决方案:模块联邦支持在运行时远程加载模块,比如从另一个域名获取组件库,可以实现 “热更新组件” 而不用重新部署主应用。
3. 避免重复打包、减小 bundle 体积
- 问题:多个应用如果都使用 React、Lodash 等库,会各自打包,浪费空间、增加加载时间。
- 解决方案:Module Federation 支持共享依赖(shared),只打包一次共享模块,多个项目共用。
4. 解耦团队开发、支持独立部署
- 问题:微前端架构中,多个团队协作开发不同模块时,耦合度高、协作成本大。
- 解决方案:模块联邦支持每个子应用独立开发、独立部署,主应用只在运行时加载,增强解耦与独立性。
举个例子:
假设你有两个项目:
app1是主应用app2是一个组件库,暴露了Button
你可以通过 Module Federation 在 app1 中直接使用 app2 的 Button:
// app1 webpack.config.js
plugins: [
new ModuleFederationPlugin({
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
}),
]
// 在 app1 中使用 app2 的 Button
import Button from 'app2/Button'
用例实现
这个 demo 模拟了上面那种典型的多个独立项目间的模块共享场景,这里新建三个项目:
home-app:应用通过 Module Federation 暴露List.tsx组件;course-app:应用通过 Module Federation 加载/消费home-app/List.tsx组件,同时通过 Module Federation 暴露自身整个App.tsx应用;act-app:应用通过 Module Federation 加载/消费course-app/App.tsx整个应用。
初始化 React App 并引导项目
首先,我们用Webpack + TypeScript创建一个React App home-app。
我们需要创建bootstrap.tsx文件用于异步加载应用。这是由于在Module Federation中,子应用的加载是动态完成的,因此我们需要一个入口文件来引导整个项目。
因为远程模块的加载是异步的,所以推荐将应用的启动逻辑放在
bootstrap.tsx中,在index.tsx中用动态import('./bootstrap')方式加载,避免ReactDOM.render提前执行。
在src目录下创建bootstrap.tsx:
import App from './App'
import React from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
const container = document.getElementById('root')
if (!container) throw new Error('Failed to find the root element')
const root = createRoot(container)
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
然后在index.tsx 中动态导入bootstrap.tsx,实现代码分离:
import('./bootstrap')
export {}
配置 Webpack 和 Module Federation 插件
在项目根目录下创建webpack.config.js文件:
const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const HtmlWebpackPlugin = require('html-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const deps = require('./package.json').dependencies
module.exports = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true,
publicPath: 'auto',
},
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'HOMEAPP',
filename: 'remoteEntry.js',
exposes: {
'./List': './src/components/List',
},
shared: [
{
react: {
requiredVersion: deps.react,
singleton: true,
eager: true,
},
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
eager: true,
},
...deps,
},
],
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
hot: true,
open: true,
},
}
插件说明
ModuleFederationPlugin 是实现微前端的关键插件:
name: 表示该应用的唯一标识(例如:HOMEAPP)。filename: 是暴露的远程入口文件。exposes: 用于指定要暴露给其他应用的模块(例如:App.tsx,List.tsx)。shared: 则声明了共享依赖,使用singleton: true可确保 React 等库只加载一次。
配置应用
在前文中我们已完成home-app的基础配置,接下来将继续完善各应用内容,并用同样的方式依次配置course-app与act-app。
home-app:暴露组件模块
在 home-app中,我们实现了一个带样式和可配置参数的 UI 列表组件 List.tsx,并将其集成进主应用 App.tsx 中展示。随后,借助ModuleFederationPlugin的exposes配置,将该组件对外暴露,供其他项目使用。
home-app端口号设置为3000。
ModuleFederationPlugin 配置伪代码:
使用exposes配置暴露模块时:
./List是对外暴露的模块名(远程访问时的引用名);./src/components/List是实际本地文件路径;
const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('./package.json').dependencies
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'HOMEAPP',
filename: 'remoteEntry.js',
exposes: {
'./List': './src/components/List',
},
shared: [
{
react: {
requiredVersion: deps.react,
singleton: true,
eager: true,
},
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
eager: true,
},
...deps,
},
],
}),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
hot: true,
open: true,
},
}
course-app:消费组件并暴露自身应用
course-app的主要作用是远程加载home-app中的List组件,并集成进自己的页面结构中。同时,为了供其他系统复用,也通过Module Federation将整个App.tsx暴露出去。
course-app端口号设置为3001。
ModuleFederationPlugin 配置伪代码:
使用exposes配置暴露模块时:
./App是对外暴露的模块名(远程访问时的引用名);
使用remote引入远程模块时:
HOMEAPP是本地使用时的引用名,HOMEAPP@.../remoteEntry.js'中HOMEAPP必须与远程项目中的name一致(在它的ModuleFederationPlugin中声明的name);- 在代码中通过
import()方式异步引入const List = React.lazy(() => import('homeApp/List'));
const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('./package.json').dependencies
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'COURSEAPP',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
remotes: {
HOMEAPP: 'HOMEAPP@http://localhost:3000/remoteEntry.js',
},
shared: [
{
react: {
requiredVersion: deps.react,
singleton: true,
eager: true,
},
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
eager: true,
},
...deps,
},
],
}),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3001,
hot: true,
open: true,
},
}
消费远程模块代码:
List为远程模块。
import React from 'react'
import './App.scss'
const List = React.lazy(() => import('HOMEAPP/List'))
import Detail from './components/Detail'
function App() {
return (
<div className="course-app">
<h1>Course App</h1>
<Detail />
<br />
<List />
</div>
)
}
export default App
act-app:加载远程应用
最后,act-app则扮演“集成者”角色,直接远程加载course-app的整个应用模块 App,并将其嵌入到本地渲染逻辑中,实现完整的远程模块嵌入效果。
act-app端口号设置为3002。
ModuleFederationPlugin 配置伪代码:
使用remote引入远程模块时:
COURSEAPP是本地使用时的引用名,COURSEAPP@.../remoteEntry.js'中COURSEAPP必须与远程项目中的name一致(在它的ModuleFederationPlugin中声明的name);- 在代码中通过
import()方式异步引入const CourseApp = React.lazy(() => import('COURSEAPP/App'));
const path = require('path')
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('./package.json').dependencies
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'ACTAPP',
filename: 'remoteEntry.js',
remotes: {
COURSEAPP: 'COURSEAPP@http://localhost:3001/remoteEntry.js',
},
shared: [
{
react: {
requiredVersion: deps.react,
singleton: true,
eager: true,
},
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
eager: true,
},
...deps,
},
],
}),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3002,
hot: true,
open: true,
},
}
消费远程模块代码:
CourseApp为远程模块。
import React from 'react'
import './App.scss'
const CourseApp = React.lazy(() => import('COURSEAPP/App'))
function App() {
return (
<div className="act-app">
<h1>Act App</h1>
<h2 className='act-Content'>Act App Content</h2>
<CourseApp />
</div>
)
}
export default App
最终实现效果图
总结回顾
本文通过一个最小可运行的 Demo,介绍了如何使用Webpack Module Federation在多个独立的React项目之间共享组件和页面。
我们从配置home-app开始,依次实现了组件的暴露、远程加载和整合渲染,在course-app中引入了单一的远程组件模块,也最终在act-app中动态展示了远程项目的内容。
如果你想快速掌握模块联邦的基本用法,并了解跨项目组件共享的实现方式,这个 Demo 是一个不错的参考起点。
如需查看完整示例代码,可访问:github.com/sparkle027/…
扩展阅读
- 由Webpack Module Federation的作者设计和开发的官方插件 module-federation:github.com/module-fede…
- 官方插件 module-federation 模块联邦示例代码:github.com/module-fede…
- 深入了解模块联邦:scriptedalchemy.medium.com/understandi…