各位前端的英雄好汉,大家好!我是今天的主讲人,咱们今天聊聊微前端架构里的大杀器——Webpack Module Federation。这玩意儿,说白了,就是解决微前端之间代码共享和版本冲突的难题的。
开场白:微前端的那些事儿
话说,微前端这概念,大家应该都不陌生。它就像把一个巨大的单体应用拆成一个个小的、独立的“微应用”。这些微应用可以由不同的团队开发、独立部署,最后拼装在一起,给用户提供完整的体验。
这样做的好处嘛,那可多了去了:
- 技术栈自由: 每个微应用可以选择自己喜欢的技术栈,不用被统一的技术栈绑死。
- 独立部署: 每个微应用可以独立发布,互不影响,大大提升了开发效率。
- 团队自治: 每个团队可以负责自己的微应用,职责清晰,更容易管理。
但是!问题也来了:
- 代码重复: 不同的微应用可能需要用到相同的组件或者工具函数,如果没有一个好的共享机制,就会出现大量的代码重复。
- 版本冲突: 不同的微应用可能依赖同一个第三方库的不同版本,如果没有一个好的版本管理机制,就会出现版本冲突,导致应用崩溃。
这时候,Webpack Module Federation 就该闪亮登场了!
Module Federation:共享的魔法
Module Federation,顾名思义,就是模块联邦。它可以让不同的 Webpack 构建的应用之间共享模块,就像联邦国家一样,每个州(微应用)可以共享资源,但又保持独立性。
Module Federation 的基本概念
在深入代码之前,我们需要先了解几个关键的概念:
- Host(宿主): 消费其他微应用暴露的模块的应用。
- Remote(远程): 暴露模块给其他微应用消费的应用。
- Shared Modules(共享模块): 可以被 Host 和 Remote 共享的模块,比如 React、Vue、Lodash 等等。
Module Federation 的工作原理
Module Federation 的核心思想是:在构建时,将 Remote 应用暴露的模块的信息(比如模块的名称、版本、入口地址)写入一个 manifest 文件中。Host 应用在运行时,会根据 manifest 文件去加载 Remote 应用暴露的模块。
代码实战:手把手教你用 Module Federation
光说不练假把式,接下来咱们就用代码来演示一下 Module Federation 的用法。
场景: 假设我们有两个微应用:app1
和 app2
。app1
是 Host,app2
是 Remote。app2
暴露一个名为 Button
的 React 组件给 app1
使用。
1. 创建两个项目
首先,创建两个空的 React 项目:
npx create-react-app app1
npx create-react-app app2
2. 配置 app2
(Remote)
进入 app2
目录,安装 webpack
和 webpack-cli
:
cd app2
npm install webpack webpack-cli --save-dev
在 app2
目录下创建一个 webpack.config.js
文件,配置如下:
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index',
mode: 'development',
devtool: 'source-map',
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
port: 3002,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
resolve: {
extensions: ['.js', '.jsx'],
},
};
name
: Remote 应用的名称,必须唯一。filename
: 生成的 manifest 文件的名称。exposes
: 暴露的模块,./Button
是模块的别名,./src/Button
是模块的实际路径。shared
: 共享的模块,这里我们共享了react
和react-dom
。singleton: true
表示只加载一个版本的react
和react-dom
。requiredVersion: deps.react
表示必须满足package.json
中定义的react
版本。
创建一个 src/Button.jsx
文件:
import React from 'react';
const Button = ({ children }) => {
return <button style={{ backgroundColor: 'lightblue' }}>{children}</button>;
};
export default Button;
修改 app2
的 package.json
文件,添加一个 build 命令:
{
"scripts": {
"start": "react-scripts start",
"build": "webpack --mode production",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
运行 npm run build
构建 app2
。
3. 配置 app1
(Host)
进入 app1
目录,安装 webpack
和 webpack-cli
:
cd ../app1
npm install webpack webpack-cli --save-dev
在 app1
目录下创建一个 webpack.config.js
文件,配置如下:
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index',
mode: 'development',
devtool: 'source-map',
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
port: 3001,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
resolve: {
extensions: ['.js', '.jsx'],
},
};
remotes
: 指定 Remote 应用的信息,app2
是 Remote 应用的名称,http://localhost:3002/remoteEntry.js
是 Remote 应用的 manifest 文件的地址。
修改 app1
的 src/App.js
文件,引入 app2
暴露的 Button
组件:
import React, { Suspense, lazy } from 'react';
const RemoteButton = lazy(() => import('app2/Button'));
function App() {
return (
<div>
<h1>App1</h1>
<Suspense fallback="Loading Button...">
<RemoteButton>Hello from App2!</RemoteButton>
</Suspense>
</div>
);
}
export default App;
- 这里使用了
React.lazy
和Suspense
来实现动态加载 Remote 组件。
修改 app1
的 package.json
文件,添加一个 build 命令:
{
"scripts": {
"start": "react-scripts start",
"build": "webpack --mode production",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
运行 npm run build
构建 app1
。
4. 运行项目
分别运行 app1
和 app2
:
# 在 app1 目录下
npm start
# 在 app2 目录下
npm start
打开浏览器,访问 http://localhost:3001
,你就能看到 app1
成功加载了 app2
暴露的 Button
组件啦!
Module Federation 的高级用法
上面的例子只是 Module Federation 的一个简单应用,它还有很多高级用法,比如:
- 动态 Remote: 可以根据不同的环境加载不同的 Remote 应用。
- 版本控制: 可以指定 Remote 应用的版本,避免版本冲突。
- 共享模块的高级配置: 可以更灵活地配置共享模块,比如指定加载策略、版本范围等等。
1. 动态 Remote
假设我们想要根据环境变量来决定加载哪个 Remote 应用。
修改 app1
的 webpack.config.js
文件:
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
const remoteUrl = process.env.REMOTE_URL || 'http://localhost:3002/remoteEntry.js';
module.exports = {
// ... 其他配置
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: `app2@${remoteUrl}`,
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
// ... 其他配置
};
在运行 app1
时,可以通过设置 REMOTE_URL
环境变量来指定 Remote 应用的地址:
REMOTE_URL=http://localhost:3003/remoteEntry.js npm start
2. 版本控制
假设 app2
有两个版本:1.0.0
和 2.0.0
。我们想要 app1
使用 app2
的 1.0.0
版本。
首先,修改 app2
的 package.json
文件,将版本设置为 1.0.0
:
{
"name": "app2",
"version": "1.0.0",
// ... 其他配置
}
构建 app2
。
然后,修改 app1
的 webpack.config.js
文件:
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
// ... 其他配置
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
// ... 其他配置
};
在 shared
中,我们可以指定共享模块的版本范围:
shared: {
react: {
singleton: true,
requiredVersion: {
// 指定 react 的版本范围
"^16.0.0": "16.x",
"^17.0.0": "17.x",
}[deps.react] || deps.react, // 默认使用 package.json 中定义的版本
},
"react-dom": {
singleton: true,
requiredVersion: {
"^16.0.0": "16.x",
"^17.0.0": "17.x",
}[deps["react-dom"]] || deps["react-dom"], // 默认使用 package.json 中定义的版本
},
},
Module Federation 的优缺点
优点:
- 代码共享: 避免了代码重复,减少了应用体积。
- 版本管理: 解决了版本冲突的问题,保证了应用的稳定性。
- 技术栈自由: 不同的微应用可以使用不同的技术栈。
- 独立部署: 每个微应用可以独立发布,互不影响。
缺点:
- 配置复杂: 需要配置 Webpack,有一定的学习成本。
- 运行时依赖: Host 应用需要在运行时加载 Remote 应用的模块,可能会影响性能。
- 调试困难: 跨应用的调试可能会比较困难。
Module Federation 的应用场景
Module Federation 适用于以下场景:
- 微前端架构: 这是 Module Federation 最典型的应用场景。
- 插件系统: 可以使用 Module Federation 来实现插件的动态加载。
- 大型单体应用: 可以使用 Module Federation 将单体应用拆分成多个模块,提高开发效率。
总结
Module Federation 是一个强大的工具,它可以帮助我们构建更加灵活、可维护的微前端架构。虽然它有一定的学习成本,但是一旦掌握了它,你就能体会到它的强大之处。
Module Federation 的配置参数详解
为了让大家更深入地了解 Module Federation,下面我们来详细地讲解一下它的配置参数。
ModuleFederationPlugin 的配置参数
参数名 | 类型 | 描述 |
---|