微前端架构: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/) 转载或引用必须申明原指尖魔法屋来源及源地址!