各位靓仔靓女们,晚上好!我是你们的老朋友,今天咱们来聊聊一个前端界的热门话题——微前端(Micro Frontends)。这玩意儿听起来高大上,其实说白了就是把一个巨大的前端应用拆成一个个小的、可以独立开发、独立部署的“积木”,然后像搭乐高一样把它们拼起来。
今天咱们的重点是微前端的演进之路,从早期的 single-spa
到现在炙手可热的 Module Federation
,看看它们是怎么一步步把前端开发带上更高境界的。
一、 为什么要搞微前端?
在深入技术细节之前,咱们先来聊聊动机。为啥要搞微前端?难道单体应用它不香吗?
其实,单体应用就像一个大胖子,刚开始的时候,身手还挺敏捷,但随着业务越来越复杂,功能越来越多,这个胖子就变得越来越臃肿,启动慢,部署慢,改动一点点东西都要重新部署整个应用,简直让人抓狂。而且,如果团队成员使用的技术栈不同,单体应用就会变成一个巨大的技术债务,难以维护。
微前端就是为了解决这些问题而生的。它可以带来以下好处:
- 独立开发和部署: 每个微前端应用都可以由一个独立的团队负责开发和部署,互不干扰。
- 技术栈无关: 每个微前端应用可以使用不同的技术栈,比如有的用 React,有的用 Vue,有的用 Angular,只要它们能最终渲染成 HTML,就都可以整合在一起。
- 更好的可维护性: 由于每个微前端应用都比较小,代码量少,逻辑清晰,所以更容易维护。
- 增量升级: 可以逐步升级应用,而不是一次性重写整个应用。
- 更快的构建和部署: 每个微前端应用的构建和部署速度都更快,可以更快地响应业务需求。
二、 初出茅庐:single-spa
single-spa
可以说是微前端领域的先驱者之一。它的核心思想是:一个页面,多个框架。简单来说,就是在一个 HTML 页面中,可以同时运行多个前端框架的应用。
single-spa
通过一个叫做 single-spa-config.js
的配置文件来管理这些微前端应用。这个配置文件定义了每个微前端应用的入口、加载方式、激活条件等等。
2.1 核心概念
- Root Config (根配置): 这是整个应用的入口,负责加载和引导所有的微前端应用。
- Applications (应用): 指的是一个个独立的微前端应用,每个应用都有自己的生命周期函数 (bootstrap, mount, unmount)。
- Activity Function (激活函数): 一个函数,用于判断当前路由是否应该激活某个微前端应用。
2.2 代码示例
首先,我们需要安装 single-spa
:
npm install single-spa --save
然后,创建一个 single-spa-config.js
文件:
import * as singleSpa from 'single-spa';
// 定义一个微前端应用
const app1 = () => import('./app1/app1.js');
const app2 = () => import('./app2/app2.js');
// 注册微前端应用
singleSpa.registerApplication(
'app1', // 应用名称
app1, // 加载应用的函数
location => location.pathname.startsWith('/app1'), // 激活函数
{ someData: 'app1 data' } // 传递给应用的 props
);
singleSpa.registerApplication(
'app2',
app2,
location => location.pathname.startsWith('/app2'),
{ someData: 'app2 data' }
);
// 启动 single-spa
singleSpa.start();
在这个例子中,我们注册了两个微前端应用:app1
和 app2
。当 URL 的路径以 /app1
开头时,app1
应用会被激活;当 URL 的路径以 /app2
开头时,app2
应用会被激活。
接下来,我们需要创建 app1
和 app2
两个微前端应用。这里以 React 应用为例:
app1/app1.js
:
import React from 'react';
import ReactDOM from 'react-dom';
let reactLifecycles = null;
async function bootstrap(props) {
console.log('app1 bootstrap', props);
return Promise.resolve();
}
async function mount(props) {
console.log('app1 mount', props);
const App = () => (
<div>
<h1>App 1</h1>
<p>Hello from App 1! Data from root: {props.someData}</p>
</div>
);
ReactDOM.render(<App />, document.getElementById('app1-container'));
return Promise.resolve();
}
async function unmount(props) {
console.log('app1 unmount', props);
ReactDOM.unmountComponentAtNode(document.getElementById('app1-container'));
return Promise.resolve();
}
reactLifecycles = {
bootstrap,
mount,
unmount,
};
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
export function render() {
if (!document.getElementById('app1-container')) {
const container = document.createElement("div");
container.id = 'app1-container';
document.body.appendChild(container);
}
mount({ someData: 'Initial data'});
}
// 如果不是 single-spa 环境,直接渲染
if (!window.singleSpaNavigate) {
render();
}
app2/app2.js
:
import React from 'react';
import ReactDOM from 'react-dom';
let reactLifecycles = null;
async function bootstrap(props) {
console.log('app2 bootstrap', props);
return Promise.resolve();
}
async function mount(props) {
console.log('app2 mount', props);
const App = () => (
<div>
<h1>App 2</h1>
<p>Hello from App 2! Data from root: {props.someData}</p>
</div>
);
ReactDOM.render(<App />, document.getElementById('app2-container'));
return Promise.resolve();
}
async function unmount(props) {
console.log('app2 unmount', props);
ReactDOM.unmountComponentAtNode(document.getElementById('app2-container'));
return Promise.resolve();
}
reactLifecycles = {
bootstrap,
mount,
unmount,
};
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
export function render() {
if (!document.getElementById('app2-container')) {
const container = document.createElement("div");
container.id = 'app2-container';
document.body.appendChild(container);
}
mount({ someData: 'Initial data'});
}
// 如果不是 single-spa 环境,直接渲染
if (!window.singleSpaNavigate) {
render();
}
最后,创建一个 index.html
文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Single-SPA Example</title>
</head>
<body>
<nav>
<a href="/app1">App 1</a>
<a href="/app2">App 2</a>
</nav>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/single-spa.min.js"></script>
<script src="./single-spa-config.js"></script>
</body>
</html>
2.3 优点和缺点
single-spa
的优点是:
- 简单易用: 只需要几行代码就可以将多个前端应用整合在一起。
- 技术栈无关: 可以支持多种前端框架。
- 灵活: 可以根据路由动态加载和卸载应用。
single-spa
的缺点是:
- 需要手动管理应用的生命周期: 需要手动编写
bootstrap
、mount
和unmount
函数。 - 共享状态困难: 微前端应用之间共享状态比较困难,需要借助外部的状态管理工具。
- 性能问题: 如果微前端应用过多,可能会导致页面加载速度变慢。
三、 厚积薄发:Module Federation
Module Federation
是 Webpack 5 引入的一个新特性,它允许我们将一个 Webpack 构建的应用拆分成多个独立的模块,这些模块可以在运行时被其他应用加载和使用。
Module Federation
最大的特点是:代码共享。它可以让我们在不同的应用之间共享代码,而不需要将代码复制到每个应用中。这大大提高了代码的复用率,减少了代码冗余。
3.1 核心概念
- Host (宿主): 消费远程模块的应用。
- Remote (远程): 提供可被其他应用消费的模块的应用。
- Shared Modules (共享模块): 在 Host 和 Remote 之间共享的模块,例如 React, ReactDOM 等。
3.2 代码示例
假设我们有两个应用:app1
和 app2
。app1
将作为 Host,app2
将作为 Remote。
首先,我们需要配置 app2
(Remote) 的 webpack.config.js
文件:
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: 'http://localhost:3001/', // 确保publicPath正确
filename: 'bundle.js',
},
devServer: {
port: 3001,
hot: true,
},
module: {
rules: [
{
test: /.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app2', // 必须唯一
filename: 'remoteEntry.js', // 暴露模块的入口文件
exposes: {
'./Button': './src/Button', // 暴露的模块,Key 是模块名,Value 是模块路径
},
shared: {
react: { singleton: true, requiredVersion: '^17.0.0' },
'react-dom': { singleton: true, requiredVersion: '^17.0.0' },
},
}),
],
};
在这个配置中,我们使用了 ModuleFederationPlugin
插件。name
字段指定了 Remote 应用的名称,filename
字段指定了暴露模块的入口文件,exposes
字段指定了要暴露的模块,shared
字段指定了要共享的模块。
接下来,我们需要创建 app1
(Host) 的 webpack.config.js
文件:
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: 'http://localhost:3000/', // 确保publicPath正确
filename: 'bundle.js',
},
devServer: {
port: 3000,
hot: true,
},
module: {
rules: [
{
test: /.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1', // 必须唯一
remotes: {
app2: 'app2@http://localhost:3001/remoteEntry.js', // 引用远程模块,Key 是模块名,Value 是远程模块的地址
},
shared: {
react: { singleton: true, requiredVersion: '^17.0.0' },
'react-dom': { singleton: true, requiredVersion: '^17.0.0' },
},
}),
],
};
在这个配置中,我们同样使用了 ModuleFederationPlugin
插件。name
字段指定了 Host 应用的名称,remotes
字段指定了要引用的远程模块,shared
字段指定了要共享的模块。
最后,我们需要在 app1
中使用 app2
暴露的 Button
组件:
app1/src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
// 注意:这里使用动态import,必须这样做
import('app2/Button').then(({ default: Button }) => {
const App = () => (
<div>
<h1>App 1</h1>
<Button text="Click me from App 2!" />
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));
});
app2/src/Button.js
:
import React from 'react';
const Button = ({ text }) => (
<button style={{ backgroundColor: 'lightblue' }}>{text}</button>
);
export default Button;
启动两个应用,你就可以在 app1
中看到 app2
提供的 Button
组件了。
3.3 优点和缺点
Module Federation
的优点是:
- 代码共享: 可以极大地提高代码的复用率,减少代码冗余。
- 独立部署: 每个应用都可以独立部署,互不影响。
- 增量升级: 可以逐步升级应用,而不需要一次性重写整个应用。
- 性能优化: 可以通过按需加载远程模块来优化页面加载速度。
Module Federation
的缺点是:
- 配置复杂: 需要配置 Webpack,学习成本较高。
- 版本冲突: 需要小心处理共享模块的版本冲突问题。
- 类型定义: 在 TypeScript 项目中,共享模块的类型定义可能会比较麻烦。
四、 总结:single-spa
vs Module Federation
特性 | single-spa |
Module Federation |
---|---|---|
核心思想 | 一个页面,多个框架 | 代码共享 |
技术栈无关 | 支持 | 支持 (需要Webpack支持) |
代码共享 | 困难,需要借助外部状态管理工具 | 容易,可以直接共享代码 |
部署方式 | 独立部署 | 独立部署 |
学习成本 | 低 | 高 |
性能 | 可能会有性能问题,需要优化 | 可以通过按需加载优化性能 |
使用场景 | 适合遗留系统的微前端改造,或技术栈差异较大的团队 | 适合大型应用的微前端拆分,或需要高度代码复用的场景 |
总的来说,single-spa
更加灵活,易于上手,但代码共享能力较弱。Module Federation
代码共享能力强大,但配置复杂,学习成本较高。选择哪种方案,需要根据具体的项目需求和团队情况来决定。
五、 未来展望
微前端技术还在不断发展,未来可能会出现更多新的解决方案。例如,基于 Web Components 的微前端方案,基于 Serverless 的微前端方案等等。
无论技术如何发展,微前端的核心思想都不会改变:将大型应用拆分成小的、可独立维护的单元,从而提高开发效率、降低维护成本。
好了,今天的分享就到这里。希望大家能够对微前端有一个更清晰的认识。如果大家还有什么问题,可以在评论区留言,我会尽力解答。
感谢大家的收听!下次再见!
补充说明:
上面的代码示例只是为了演示 single-spa
和 Module Federation
的基本用法,实际项目中还需要考虑更多因素,例如:
- 路由管理: 如何在微前端应用之间进行路由跳转?
- 状态管理: 如何在微前端应用之间共享状态?
- 通信: 如何在微前端应用之间进行通信?
- 构建和部署: 如何自动化构建和部署微前端应用?
- 安全性: 如何保证微前端应用的安全性?
这些问题都需要根据具体的项目需求来选择合适的解决方案。希望大家在实践中不断探索,不断学习,共同推动微前端技术的发展。