各位观众老爷,晚上好!今天咱们聊点儿时髦的,关于前端架构的——微前端。尤其是,用Webpack的Module Federation这个“大杀器”来实现微前端。这玩意儿,说白了,就是把一个巨无霸前端应用拆成一堆小而美的应用,然后像搭积木一样拼起来。
开场白:为什么要微前端?
想象一下,你有一个巨大的单体前端应用,代码库像一座喜马拉雅山,每次改动都像攀登珠穆朗玛峰,发布一次都要战战兢兢,生怕雪崩。新团队加入,光是熟悉代码就要耗费几个月。这,就是单体应用的痛。
微前端,就是为了解决这个问题。它可以:
- 解耦业务: 不同团队负责不同的业务模块,互不干扰,开发效率更高。
- 技术栈自由: 每个微应用可以使用不同的技术栈,老项目可以逐步迁移,新项目可以拥抱新技术。
- 独立部署: 每个微应用可以独立部署,快速迭代,减少发布风险。
- 提升可维护性: 小而美的代码库,更容易维护和测试。
主角登场:Webpack Module Federation
Module Federation,是Webpack 5推出的一个重量级特性。它允许不同的Webpack构建的应用,共享彼此的代码,就像共享DLL一样。但是,它比DLL更灵活,更动态。
Module Federation的核心概念:
- Host (容器应用): 负责加载和管理微应用。
- Remote (微应用): 对外暴露可共享的模块。
- Shared Modules (共享模块): 被Host和Remote共享的依赖。
实战演练:搭建一个简单的微前端应用
咱们来撸起袖子,搭一个简单的微前端应用。这个应用包含一个Host应用,和两个Remote应用(app1和app2)。
1. 项目初始化
首先,创建项目目录:
mkdir micro-frontend
cd micro-frontend
mkdir host app1 app2
然后,在每个目录下,初始化一个Webpack项目:
cd host
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save
cd ../app1
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save
cd ../app2
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save
cd ..
2. Host应用 (host/webpack.config.js)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: {
port: 8080,
},
output: {
publicPath: 'http://localhost:8080/',
},
module: {
rules: [
{
test: /.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:8081/remoteEntry.js',
app2: 'app2@http://localhost:8082/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '>=16.8.0' },
'react-dom': { singleton: true, requiredVersion: '>=16.8.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
代码解释:
name
: 当前应用的名称,必须唯一。remotes
: 定义需要加载的微应用。app1@http://localhost:8081/remoteEntry.js
表示从http://localhost:8081/remoteEntry.js
加载名为app1
的微应用。remoteEntry.js
是微应用暴露的入口文件。shared
: 定义共享的依赖。singleton: true
确保只加载一个版本的React和ReactDOM。requiredVersion: '>=16.8.0'
指定需要的版本范围。
3. Host应用 (host/src/index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
4. Host应用 (host/src/App.js)
import React, { Suspense } from 'react';
const App1 = React.lazy(() => import('app1/App'));
const App2 = React.lazy(() => import('app2/App'));
const App = () => (
<div>
<h1>Host Application</h1>
<Suspense fallback={<div>Loading App1...</div>}>
<App1 />
</Suspense>
<Suspense fallback={<div>Loading App2...</div>}>
<App2 />
</Suspense>
</div>
);
export default App;
代码解释:
React.lazy
: 用于懒加载微应用。只有当组件被渲染时,才会加载对应的代码。Suspense
: 用于显示加载状态。当微应用正在加载时,会显示fallback
中的内容。
5. Host应用 (host/public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Host Application</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
6. Remote应用 (app1/webpack.config.js)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
devServer: {
port: 8081,
},
output: {
publicPath: 'http://localhost:8081/',
},
module: {
rules: [
{
test: /.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-env'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
exposes: {
'./App': './src/App',
},
shared: {
react: { singleton: true, requiredVersion: '>=16.8.0' },
'react-dom': { singleton: true, requiredVersion: '>=16.8.0' },
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
代码解释:
name
: 当前应用的名称,必须唯一。exposes
: 定义需要暴露的模块。'./App': './src/App'
表示将src/App.js
暴露为App
模块。shared
: 定义共享的依赖。
7. Remote应用 (app1/src/index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
8. Remote应用 (app1/src/App.js)
import React from 'react';
const App = () => (
<div>
<h2>App1</h2>
<p>This is App1 content.</p>
</div>
);
export default App;
9. Remote应用 (app1/public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App1</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
10. Remote应用 (app2/webpack.config.js), (app2/src/index.js), (app2/src/App.js), (app2/public/index.html)
App2 的配置和代码与 App1 类似,只需将端口号改为 8082,应用名称改为 app2,内容稍作修改即可。这里不再赘述,节省篇幅。
11. 添加 Babel 配置 (host, app1, app2 目录下都添加 .babelrc)
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
12. package.json 添加启动命令 (host, app1, app2 目录下都添加)
"scripts": {
"start": "webpack serve"
},
13. 运行项目
分别在 host
, app1
, app2
目录下运行 npm start
。
然后,在浏览器中打开 http://localhost:8080
,你将会看到 Host 应用,其中包含了 App1 和 App2 的内容。
代码总结:
文件名 | 描述 |
---|---|
host/webpack.config.js | Host 应用的 Webpack 配置,定义了 remotes 和 shared 。 |
host/src/App.js | Host 应用的 React 组件,使用 React.lazy 和 Suspense 加载微应用。 |
app1/webpack.config.js | App1 应用的 Webpack 配置,定义了 exposes 和 shared 。 |
app1/src/App.js | App1 应用的 React 组件,对外暴露。 |
.babelrc (host, app1, app2 目录下) | Babel 配置文件,定义了预设。 |
Module Federation 的高级用法
上面的例子只是一个入门级的演示,Module Federation 的功能远不止于此。
- 版本控制: 可以指定共享依赖的版本范围,避免版本冲突。
- 动态加载: 可以根据需要动态加载微应用,提高性能。
- 代码分割: 可以将微应用的代码分割成多个 chunk,按需加载。
- 自定义加载器: 可以自定义加载器,加载不同类型的模块。
踩坑指南:Module Federation 的常见问题
- 版本冲突: 共享依赖的版本不一致,导致运行时错误。解决办法是明确指定版本范围,并使用
singleton: true
确保只加载一个版本。 - 循环依赖: 微应用之间存在循环依赖,导致加载失败。解决办法是重新设计模块依赖关系,避免循环依赖。
- 类型定义: 如果微应用使用了 TypeScript,需要共享类型定义,否则会出现类型错误。解决办法是使用
declaration: true
生成类型定义文件,并共享这些文件。 - CORS 问题: 如果微应用部署在不同的域名下,可能会遇到 CORS 问题。解决办法是在服务器端配置 CORS 策略。
- 性能问题: 加载过多的微应用会影响性能。解决办法是优化代码,减少依赖,并使用懒加载和代码分割。
微前端的架构模式
除了 Module Federation,还有其他的微前端架构模式,例如:
- Iframe: 最简单的微前端实现方式,但隔离性太强,通信成本高。
- Web Components: 可以使用 Web Components 将微应用封装成自定义元素,然后在 Host 应用中使用。
- Single-SPA: 一个专门用于构建微前端应用的框架,提供了路由、生命周期管理等功能。
- Qiankun: 阿里开源的微前端框架,基于 Single-SPA,提供了更完善的功能和更易用的 API。
各种方案的对比:
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Iframe | 隔离性强,技术栈无关。 | 通信成本高,SEO 不友好,用户体验差。 | 老项目改造,技术栈差异大,对隔离性要求高的场景。 |
Web Components | 可以将微应用封装成自定义元素,易于集成。 | 需要浏览器支持 Web Components,学习成本高。 | 新项目,技术栈统一,对组件化要求高的场景。 |
Single-SPA | 提供了路由、生命周期管理等功能,功能完善。 | 学习成本高,需要对 Single-SPA 的原理有深入了解。 | 中大型项目,需要统一的微前端框架。 |
Qiankun | 基于 Single-SPA,提供了更完善的功能和更易用的 API,上手快。 | 依赖 Single-SPA,需要了解 Single-SPA 的原理。 | 中大型项目,需要快速搭建微前端应用。 |
Module Federation | 代码共享,性能好,易于集成。 | 需要使用 Webpack 5,对 Webpack 的配置有一定要求,共享模块需要仔细管理。 | 新项目,技术栈统一,对性能要求高的场景。 |
总结:微前端的未来
微前端是一种非常有前景的前端架构模式,它可以解决单体应用的痛点,提高开发效率和可维护性。虽然微前端还面临一些挑战,例如版本管理、状态管理、通信等,但随着技术的发展,这些问题将会得到解决。
Module Federation 作为一种新兴的微前端实现方式,具有代码共享、性能好、易于集成等优点,越来越受到开发者的青睐。相信在不久的将来,Module Federation 将会在微前端领域发挥更大的作用。
好了,今天的讲座就到这里,希望对大家有所帮助。感谢各位的观看!