AgentSkillsCN

micro-frontends

微前端架构——组合方法、模块联邦、Single-SPA、Web组件、共享状态与应用间通信。全面覆盖将前端拆分至独立团队时的各类模式与权衡。 适用场景:微前端架构、模块联邦、Single-SPA、运行时组合、独立前端部署、跨团队前端开发。 不适用场景:单团队SPA开发(使用SPA)、服务器端渲染(使用SSR)、后端服务分解(使用Dev/Architecture/Microservices)。

SKILL.md
--- frontmatter
name: micro-frontends
description: |
    Micro-frontend architecture — composition approaches, Module Federation, single-spa, web components, shared state, and inter-app communication. Covers the patterns and tradeoffs for splitting a frontend across independent teams.
    USE FOR: micro-frontend architecture, Module Federation, single-spa, runtime composition, independent frontend deployment, cross-team frontend development
    DO NOT USE FOR: single-team SPA development (use spa), server-side rendering (use ssr), backend service decomposition (use dev/architecture/microservices)
license: MIT
metadata:
  displayName: "Micro-Frontend Architecture"
  author: "Tyler-R-Kendrick"
compatibility: claude, copilot, cursor

Micro-Frontend Architecture

Overview

Micro-frontends extend the microservices idea to the frontend: independently developed, tested, and deployed frontend applications that are composed into a single, cohesive user experience. Each team owns a vertical slice of the product — from the UI layer down through the API — with autonomy over their tech stack, build pipeline, and release cadence. The approach trades simplicity for team scalability.

Composition Approaches

code
┌──────────────────────────────────────────────────────────────────┐
│                  Micro-Frontend Composition                       │
│                                                                   │
│  Build-Time             Runtime                  Server-Side      │
│  ┌──────────────┐       ┌──────────────────┐     ┌────────────┐  │
│  │ npm packages │       │ Module Federation│     │ ESI / SSI  │  │
│  │ Monorepo     │       │ Import Maps      │     │ SSR Comp.  │  │
│  │ imports      │       │ iframes          │     │ Tailor /   │  │
│  │              │       │ Web Components   │     │ Podium     │  │
│  └──────────────┘       └──────────────────┘     └────────────┘  │
│                                                                   │
│  + Type safety            + Independent deploy    + SEO-friendly  │
│  + Optimized bundle       + Tech-stack freedom    + Fast FCP      │
│  - Coupled releases       - Runtime overhead      - Complexity    │
│  - Single deploy          - Larger bundles        - Server infra  │
└──────────────────────────────────────────────────────────────────┘

Build-Time Composition

Each micro-frontend is published as an npm package and consumed by a host application at build time.

json
// package.json of the host/shell application
{
  "dependencies": {
    "@org/header": "^2.1.0",
    "@org/product-catalog": "^3.4.0",
    "@org/checkout": "^1.8.0",
    "@org/user-profile": "^2.0.0"
  }
}

Tradeoff: Strong type safety and optimized bundles, but all micro-frontends must be built and deployed together — defeating much of the independence benefit.

Runtime Composition — Module Federation

Webpack 5 Module Federation (and Rspack) allows applications to share code at runtime. A "host" application dynamically loads "remote" modules from independently deployed bundles.

code
┌──────────────────────────────────────────────────────────┐
│  Host (Shell Application)                                 │
│                                                           │
│  Loads at runtime:                                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │
│  │ Remote A     │  │ Remote B     │  │ Remote C     │      │
│  │ (Product)    │  │ (Cart)       │  │ (Profile)    │      │
│  │ React 18     │  │ Vue 3        │  │ React 18     │      │
│  │ Port 3001    │  │ Port 3002    │  │ Port 3003    │      │
│  └─────────────┘  └─────────────┘  └─────────────┘      │
│                                                           │
│  Shared: react, react-dom (singleton)                     │
└──────────────────────────────────────────────────────────┘

Runtime Composition — Import Maps

Native browser import maps allow mapping bare module specifiers to URLs, enabling runtime loading without a bundler federation plugin.

html
<script type="importmap">
{
  "imports": {
    "@org/header": "https://cdn.example.com/header/v2.1.0/index.js",
    "@org/product": "https://cdn.example.com/product/v3.4.0/index.js",
    "react": "https://cdn.example.com/react/18.2.0/react.production.min.js"
  }
}
</script>

Runtime Composition — iframes

The simplest (and oldest) isolation approach. Each micro-frontend runs in its own iframe with full CSS and JS isolation. Drawbacks: no shared DOM, awkward communication, SEO-invisible, accessibility challenges.

Server-Side Composition

Fragments are assembled on the server before sending HTML to the client. Approaches include Edge Side Includes (ESI), Server-Side Includes (SSI), and frameworks like Podium (Node.js) or Piral.

Module Federation (Webpack 5 / Rspack)

Remote Configuration

javascript
// webpack.config.js — Remote (Product Team)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  output: {
    publicPath: 'https://product.example.com/',
    uniqueName: 'product',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'product',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
        './productApi': './src/api/products',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        '@tanstack/react-query': { singleton: true },
      },
    }),
  ],
};

Host Configuration

javascript
// webpack.config.js — Host (Shell Application)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        product: 'product@https://product.example.com/remoteEntry.js',
        cart: 'cart@https://cart.example.com/remoteEntry.js',
        profile: 'profile@https://profile.example.com/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' },
        '@tanstack/react-query': { singleton: true },
      },
    }),
  ],
};

Dynamic Remotes

typescript
// Load remotes dynamically based on configuration
async function loadRemoteModule(scope: string, module: string) {
  // Initialize sharing scope
  await __webpack_init_sharing__('default');

  const container = window[scope];
  await container.init(__webpack_share_scopes__.default);

  const factory = await container.get(module);
  return factory();
}

// Usage — load a remote component at runtime
const ProductList = React.lazy(() =>
  loadRemoteModule('product', './ProductList')
);

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <ProductList />
    </Suspense>
  );
}

single-spa

single-spa is a framework-agnostic micro-frontend orchestrator. It manages the lifecycle of multiple frontend applications (called "applications" or "parcels") within a single page.

Core Concepts

  • Applications: Full page-level micro-frontends mounted/unmounted based on URL routes
  • Parcels: Framework-agnostic components that can be mounted anywhere, regardless of route
  • Layout Engine: Declarative HTML-based routing for micro-frontends

Root Config

javascript
// root-config.js
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: '@org/navbar',
  app: () => System.import('@org/navbar'),
  activeWhen: () => true, // always active
});

registerApplication({
  name: '@org/products',
  app: () => System.import('@org/products'),
  activeWhen: '/products',
  customProps: { authToken: getAuthToken() },
});

registerApplication({
  name: '@org/checkout',
  app: () => System.import('@org/checkout'),
  activeWhen: '/checkout',
});

start();

single-spa Application (React)

javascript
// @org/products — single-spa-react wrapper
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import singleSpaReact from 'single-spa-react';
import App from './App';

const lifecycles = singleSpaReact({
  React,
  ReactDOMClient,
  rootComponent: App,
  errorBoundary(err, info, props) {
    return <div>Error loading Products module</div>;
  },
});

export const { bootstrap, mount, unmount } = lifecycles;

Web Components as Micro-Frontend Boundaries

Web Components provide native browser encapsulation (Shadow DOM) that makes them excellent micro-frontend boundaries — each team ships a custom element that encapsulates its framework and styles.

typescript
// Product team ships a custom element
class ProductCard extends HTMLElement {
  private root: ShadowRoot;

  constructor() {
    super();
    this.root = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    const productId = this.getAttribute('product-id');
    // Mount React/Vue/Svelte inside the shadow DOM
    this.render(productId);
  }

  disconnectedCallback() {
    // Cleanup framework instance
  }

  static get observedAttributes() {
    return ['product-id'];
  }

  attributeChangedCallback(name: string, oldVal: string, newVal: string) {
    if (name === 'product-id') this.render(newVal);
  }

  private render(productId: string | null) {
    // Render framework component into shadow root
  }
}

customElements.define('product-card', ProductCard);

Usage in any framework or plain HTML:

html
<product-card product-id="abc-123"></product-card>

Shared State and Communication

Micro-frontends are independently deployed, so they cannot share in-memory state directly. Communication strategies:

Custom Events (Recommended)

typescript
// Publishing micro-frontend
window.dispatchEvent(
  new CustomEvent('cart:item-added', {
    detail: { productId: 'abc-123', quantity: 1 },
  })
);

// Consuming micro-frontend
window.addEventListener('cart:item-added', (event: CustomEvent) => {
  const { productId, quantity } = event.detail;
  updateCartBadge(productId, quantity);
});

Shared Observable (Event Bus)

typescript
// shared-state.ts — published as a shared package
import { BehaviorSubject } from 'rxjs';

export interface AppState {
  user: User | null;
  cartCount: number;
  theme: 'light' | 'dark';
}

export const appState$ = new BehaviorSubject<AppState>({
  user: null,
  cartCount: 0,
  theme: 'light',
});

// Any micro-frontend can subscribe
appState$.subscribe((state) => {
  console.log('Cart count:', state.cartCount);
});

// Any micro-frontend can update
appState$.next({ ...appState$.value, cartCount: 5 });

URL-Based Communication

Use URL search params or path segments as shared state. Each micro-frontend reads from and writes to the URL — the URL becomes the single source of truth.

typescript
// Product micro-frontend sets filter
const url = new URL(window.location.href);
url.searchParams.set('category', 'electronics');
url.searchParams.set('sort', 'price-asc');
window.history.pushState({}, '', url);

// Sidebar micro-frontend reads filter
const params = new URLSearchParams(window.location.search);
const category = params.get('category');
const sort = params.get('sort');

Tradeoffs

DimensionBenefitCost
Team AutonomyTeams choose their own framework, tooling, release cadenceInconsistent UX without strong design system governance
Independent DeploymentShip features without coordinating with other teamsVersioning complexity, integration testing burden
Tech-Stack FreedomAdopt new frameworks incrementally, avoid full rewritesMultiple frameworks = larger aggregate bundle, more to learn
Fault IsolationOne micro-frontend crashing does not bring down the pageError boundaries and fallbacks add complexity
Bundle SizeEach team optimizes their own bundleDuplicate dependencies if sharing is not configured correctly
RoutingEach team owns their routesCross-app navigation, deep linking, and shared layouts are harder
Shared DependenciesModule Federation singleton sharing reduces duplicationVersion mismatches can cause subtle runtime errors
Organizational FitScales with the number of teams (Conway's Law)Overkill for small teams — adds overhead without benefit

When to Use Micro-Frontends

Use when:

  • Multiple teams (3+) work on the same user-facing application
  • Teams need independent deployment and release cadences
  • You are migrating a large legacy frontend incrementally (strangler fig pattern)
  • Different parts of the app have fundamentally different requirements (e.g., a dashboard vs. a content editor)

Do not use when:

  • A single team owns the entire frontend
  • The application is small to medium in scope
  • You can solve the problem with a well-organized monorepo and code splitting
  • You lack the infrastructure for independent builds, deployments, and integration testing

Best Practices

  • Establish a shared design system (component library, tokens, CSS variables) — team autonomy does not mean visual chaos.
  • Use Module Federation's singleton: true for critical shared dependencies (React, router) to avoid duplicate instances.
  • Define clear contracts between micro-frontends — custom events with typed payloads, not ad-hoc DOM coupling.
  • Invest in integration testing — test the composed page, not just individual micro-frontends in isolation.
  • Start with a monolith and extract micro-frontends only when organizational pain (release coordination, merge conflicts, team coupling) justifies the architectural complexity.
  • Use a shell/host application that owns the layout, authentication, and top-level routing — micro-frontends own their content area only.
  • Monitor aggregate bundle size — without vigilance, micro-frontends will independently add dependencies until the total payload dwarfs a monolith.