微前端架构:iframe不够用了之后
边界条件才会告诉你方案能不能留。
这次做微前端改造,从单体应用到微前端,。
最初的问题
单体应用
// 单体应用的问题
// 1. 应用体积大,加载慢
// 2. 部署慢,一个小改动需要部署整个应用
// 3. 团队协作困难,多人修改同一代码库
// 4. 技术栈受限,无法使用不同框架
// 示例:路由配置
const routes = [
{ path: '/home', component: Home },
{ path: '/products', component: Products },
{ path: '/orders', component: Orders },
{ path: '/users', component: Users },
// 所有路由都在一个文件中
];
// 所有组件都在一个项目中
// 一个团队的改动可能影响其他团队
iframe 方案
基础 iframe
<!DOCTYPE html>
<html>
<head>
<title>Micro Frontend - iframe</title>
</head>
<body>
<nav>
<a href="#products">Products</a>
<a href="#orders">Orders</a>
</nav>
<iframe id="micro-frontend" src="https://products.example.com" width="100%" height="600"></iframe>
</body>
</html>
iframe 通信
// 主应用
const iframe = document.getElementById('micro-frontend');
// 发送消息到 iframe
iframe.contentWindow.postMessage({
type: 'update-user',
user: currentUser
}, 'https://products.example.com');
// 接收来自 iframe 的消息
window.addEventListener('message', (event) => {
if (event.origin !== 'https://products.example.com') {
return;
}
if (event.data.type === 'product-added') {
updateCart(event.data.product);
}
});
// 子应用(iframe 内)
// 接收来自父应用的消息
window.addEventListener('message', (event) => {
if (event.data.type === 'update-user') {
currentUser = event.data.user;
updateUserInterface();
}
});
// 发送消息到父应用
function onProductAdded(product) {
window.parent.postMessage({
type: 'product-added',
product: product
}, 'https://main.example.com');
}
Web Components 方案
自定义元素
// 定义微前端组件
class ProductsComponent extends HTMLElement {
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<div class="products-container">
<h2>Products</h2>
<div class="products-list"></div>
</div>
<style>
.products-container {
padding: 20px;
}
.products-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
</style>
`;
}
}
// 注册自定义元素
customElements.define('products-component', ProductsComponent);
// 使用微前端组件
const main = document.getElementById('main');
const productsComponent = document.createElement('products-component');
main.appendChild(productsComponent);
微前端框架
// 使用 single-spa
import { registerApplication, start } from 'single-spa';
// 注册产品应用
registerApplication({
name: 'products',
app: () => System.import('products'),
activeWhen: '/products',
customProps: {
domElementGetter: () => document.getElementById('products-container')
}
});
// 注册订单应用
registerApplication({
name: 'orders',
app: () => System.import('orders'),
activeWhen: '/orders',
customProps: {
domElementGetter: () => document.getElementById('orders-container')
}
});
// 启动微前端
start();
Module Federation 方案
配置主应用
// 主应用 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
entry: './src/index.js',
plugins: [
new ModuleFederationPlugin({
name: 'main',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
orders: 'orders@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true }
}
})
]
};
// 主应用使用远程模块
import Products from 'products/Products';
function App() {
return (
<div>
<nav>
<a href="#products">Products</a>
<a href="#orders">Orders</a>
</nav>
<Products />
</div>
);
}
配置子应用
// 产品应用 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./Products': './src/Products'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// 产品应用组件
import React from 'react';
export default function Products() {
return (
<div>
<h2>Products</h2>
{/* 产品列表 */}
</div>
);
}
微前端框架选择
比较不同方案
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| iframe | 完全隔离 | 性能差,通信困难 | 简单场景 |
| Web Components | 标准技术 | 浏览器支持 | 现代浏览器 |
| single-spa | 成熟框架 | 配置复杂 | 大型项目 |
| Module Federation | 现代化方案 | 学习成本 | React 应用 |
选择建议
小型项目:使用 iframe 或 Web Components
中型项目:使用 single-spa
大型项目:使用 Module Federation
React 项目:优先使用 Module Federation
通信机制
事件总线
// 事件总线
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
// 创建全局事件总线
const eventBus = new EventBus();
// 主应用使用
eventBus.on('user-logged-in', (user) => {
updateAllApplications(user);
});
// 子应用使用
eventBus.emit('user-logged-in', currentUser);
状态共享
// 使用 RxJS 共享状态
import { BehaviorSubject } from 'rxjs';
class SharedState {
constructor() {
this.currentUser$ = new BehaviorSubject(null);
this.cart$ = new BehaviorSubject([]);
}
setCurrentUser(user) {
this.currentUser$.next(user);
}
getCurrentUser() {
return this.currentUser$.asObservable();
}
addToCart(product) {
const currentCart = this.cart$.value;
this.cart$.next([...currentCart, product]);
}
getCart() {
return this.cart$.asObservable();
}
}
// 创建共享状态实例
const sharedState = new SharedState();
// 主应用
sharedState.getCurrentUser().subscribe(user => {
console.log('Current user:', user);
});
// 子应用
sharedState.getCurrentUser().subscribe(user => {
updateUserInterface(user);
});
sharedState.addToCart(product);
样式隔离
CSS Modules
/* Products.module.css */
.container {
padding: 20px;
}
.title {
color: blue;
}
import styles from './Products.module.css';
export default function Products() {
return (
<div className={styles.container}>
<h2 className={styles.title}>Products</h2>
</div>
);
}
Styled Components
import styled from 'styled-components';
const Container = styled.div`
padding: 20px;
`;
const Title = styled.h2`
color: blue;
`;
export default function Products() {
return (
<Container>
<Title>Products</Title>
</Container>
);
}
Shadow DOM
class ProductsComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.container {
padding: 20px;
}
.title {
color: blue;
}
</style>
<div class="container">
<h2 class="title">Products</h2>
</div>
`;
}
}
customElements.define('products-component', ProductsComponent);
踩过的坑
坑一:路由冲突
不同子应用的路由冲突。
解决:使用路由前缀或绝对路径。
// 使用路由前缀
const routes = [
{ path: '/products', component: Products },
{ path: '/orders', component: Orders }
];
// 或者在子应用中使用绝对路径
const ProductsRoutes = [
{ path: '/list', component: ProductList },
{ path: '/detail/:id', component: ProductDetail }
];
// 在主应用中使用
const routes = [
{
path: '/products',
children: ProductsRoutes
}
];
坑二:样式冲突
不同子应用的样式互相影响。
解决:使用样式隔离技术。
// 使用 CSS Modules 或 Styled Components
import styles from './Products.module.css';
// 或使用 Shadow DOM
const shadowRoot = element.attachShadow({ mode: 'open' });
坑三:依赖版本冲突
不同子应用使用不同版本的依赖。
解决:使用 Module Federation 的共享依赖功能。
new ModuleFederationPlugin({
name: 'main',
shared: {
react: {
singleton: true,
requiredVersion: deps.react,
eager: true
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
eager: true
}
}
});
写在最后
微前端这东西,不只是技术,是组织架构和团队协作。
解决了:
- 应用体积问题
- 部署效率问题
- 团队协作问题
- 技术栈限制
带来了:
- 复杂度增加
- 学习成本
- 维护成本
实施之前先评估:
- 应用规模
- 团队数量
- 技术能力
- 预算
不是所有应用都需要微前端,有时候单体应用更简单。
这次微前端改造花了一个月,从 iframe 到 Module Federation。改造完成后,部署时间从 30 分钟降到 5 分钟,团队协作效率提升了 40%。
版权声明: 本文首发于 指尖魔法屋-微前端架构:iframe不够用了之后(https://blog.thinkmoon.cn/post/87-micro-frontend-architecture-iframemodule-federation-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。