每天一个高级前端知识 - Day 23

2 阅读5分钟

每天一个高级前端知识 - Day 23

今日主题:前端架构演进 - 从单体到微前端,再到岛屿架构

核心概念:架构的演进是为了解决规模带来的复杂度

随着应用规模和团队规模的扩大,前端架构经历了从单体应用微前端岛屿架构的演进。2024-2025年,岛屿架构正在成为大型内容型网站的新选择。

🔬 架构演进路线图

第一代:单体应用 (Monolith)
├── 优势:简单直接,开发初期效率高
├── 劣势:代码耦合,团队协作困难,性能瓶颈
└── 代表:传统 jQuery/React SPA

第二代:微前端 (Micro-Frontend)
├── 优势:技术栈无关,独立部署,团队自治
├── 劣势:复杂度高,重复依赖,样式隔离困难
└── 代表:qiankun、Module Federation、无界

第三代:岛屿架构 (Islands Architecture) ← 当前趋势
├── 优势:性能极佳,渐进式增强,SEO友好
├── 劣势:需要框架支持,学习曲线
└── 代表:Astro、Fresh、Next.js Partial Prerendering

🏝️ 岛屿架构深度解析

核心思想:在静态HTML中注入独立的交互"岛屿",每个岛屿都是独立的客户端组件。

┌─────────────────────────────────────────────────────────┐
│                      静态 HTML                           │
│  ┌─────────────────────────────────────────────────┐    │
│  │  🏝️ 评论区岛屿 (交互式)                          │    │
│  │  • React 组件 • 独立加载 • 懒执行                │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  静态内容(Markdown/文本)                               │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  🏝️ 购物车岛屿 (交互式)                          │    │
│  │  • Vue 组件 • 独立状态                           │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  🏝️ 订阅表单岛屿 (交互式)                        │    │
│  │  • Preact 组件 • 极轻量                         │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

⚡ Astro 岛屿架构实战

---
// ============ Astro 组件(默认零JS) ============
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import ProductList from '../components/ProductList.astro';
import Cart from '../components/Cart.tsx';
import Comments from '../components/Comments.vue';
import Newsletter from '../components/Newsletter.svelte';

// --- 静态数据(构建时或请求时获取)---
const products = await fetchProducts();
const comments = await fetchComments();
---

<Layout title="岛屿架构示例">
  <Header />
  
  <!-- 静态内容,零JS -->
  <main>
    <section class="hero">
      <h1>欢迎来到岛屿架构网站</h1>
      <p>这是静态HTML,没有任何JavaScript负担</p>
    </section>
    
    <!-- 🏝️ ProductList 岛屿 - 使用 Astro 组件(无JS) -->
    <!-- 纯 HTML/CSS 渲染的产品列表 -->
    <ProductList products={products} />
    
    <!-- 🏝️ Cart 岛屿 - React 组件 -->
    <!-- 仅在可见时加载 React 运行时 -->
    <Cart client:visible client:load="eager" />
    
    <!-- 🏝️ Comments 岛屿 - Vue 组件 -->
    <!-- 空闲时加载,不阻塞主线程 -->
    <Comments comments={comments} client:idle />
    
    <!-- 🏝️ Newsletter 岛屿 - Svelte 组件 -->
    <!-- 仅在交互时加载 -->
    <Newsletter client:only="svelte" />
  </main>
</Layout>

<script>
  // 只有这个script会打包到客户端
  console.log('这是全局脚本,但非常小');
</script>
// ============ React 岛屿组件 ============
// src/components/Cart.tsx
import { useState, useEffect } from 'react';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

export default function Cart() {
  const [items, setItems] = useState<CartItem[]>([]);
  const [isOpen, setIsOpen] = useState(false);
  
  useEffect(() => {
    // 仅在客户端执行
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
      setItems(JSON.parse(savedCart));
    }
  }, []);
  
  const addItem = (product: any) => {
    const newItems = [...items, { ...product, quantity: 1 }];
    setItems(newItems);
    localStorage.setItem('cart', JSON.stringify(newItems));
  };
  
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  
  return (
    <div className="cart">
      <button 
        onClick={() => setIsOpen(!isOpen)}
        className="cart-button"
      >
        🛒 购物车 ({items.length})
      </button>
      
      {isOpen && (
        <div className="cart-dropdown">
          {items.length === 0 ? (
            <p>购物车是空的</p>
          ) : (
            <>
              {items.map(item => (
                <div key={item.id} className="cart-item">
                  <span>{item.name}</span>
                  <span>¥{item.price} x {item.quantity}</span>
                </div>
              ))}
              <div className="cart-total">
                总计: ¥{total.toFixed(2)}
              </div>
              <button className="checkout-btn">
                去结算
              </button>
            </>
          )}
        </div>
      )}
    </div>
  );
}

🔧 岛屿架构配置与优化

// ============ astro.config.mjs ============
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';
import tailwind from '@astrojs/tailwind';
import compress from 'astro-compress';

export default defineConfig({
  // 集成多个框架
  integrations: [
    react({
      // React 18 并发特性
      experimentalReactChildren: true,
    }),
    vue(),
    svelte(),
    tailwind(),
    compress()
  ],
  
  // 输出模式:静态或服务端渲染
  output: 'hybrid', // 混合模式,部分静态部分SSR
  
  // 岛屿配置
  experimental: {
    // 优化岛屿预加载
    optimizeHoistedScript: true,
    // 部分水合
    clientPrerender: true
  },
  
  // 构建优化
  build: {
    // 内联较小的样式
    inlineStylesheets: 'auto',
    // 分块策略
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'vue-vendor': ['vue'],
          'svelte-vendor': ['svelte']
        }
      }
    }
  },
  
  // 性能预算
  vite: {
    build: {
      chunkSizeWarningLimit: 100, // 100KB
      assetsInlineLimit: 4096 // 4KB
    }
  }
});

🎯 岛屿架构最佳实践

---
// ============ 性能优化模式 ============
// src/components/OptimizedIsland.astro

// 1. 懒加载策略
// client:load - 立即加载
// client:idle - 空闲时加载
// client:visible - 可见时加载
// client:media - 媒体查询匹配时加载
// client:only - 仅客户端渲染(跳过SSR)

// 2. 条件加载
const isMobile = Astro.request.headers.get('user-agent')?.includes('Mobile');
---

<!-- 移动端加载轻量组件 -->
{isMobile ? (
  <MobileCarousel client:visible />
) : (
  <DesktopCarousel client:visible />
)}

<!-- 仅在需要时加载重量级组件 -->
<HeavyChart client:visible />

<!-- 优先加载关键岛屿 -->
<CriticalIsland client:load />

<!-- 延迟加载非关键岛屿 -->
<NonCriticalIsland client:idle />
// ============ 岛屿通信 ============
// 岛屿之间可以通过事件总线通信
// src/lib/eventBus.ts
type EventCallback = (data: any) => void;

class EventBus {
  private listeners = new Map<string, EventCallback[]>();
  
  on(event: string, callback: EventCallback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }
  
  off(event: string, callback: EventCallback) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index !== -1) callbacks.splice(index, 1);
    }
  }
  
  emit(event: string, data: any) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach(cb => cb(data));
    }
  }
}

export const eventBus = new EventBus();

// React 岛屿中
// src/components/ProductCard.tsx
import { eventBus } from '../lib/eventBus';

export default function ProductCard({ product }) {
  const addToCart = () => {
    eventBus.emit('cart:add', product);
  };
  
  return <button onClick={addToCart}>加入购物车</button>;
}

// Svelte 岛屿中
// src/components/Cart.svelte
import { eventBus } from '../lib/eventBus';
import { onMount } from 'svelte';

let items = [];

onMount(() => {
  eventBus.on('cart:add', (product) => {
    items = [...items, product];
  });
});
</script>

<div class="cart">
  购物车 ({items.length})
</div>

📊 架构对比与选型建议

架构首屏JS交互响应开发体验SEO适用场景
单体SPA大(~200KB)后台管理系统
微前端中(~100KB)大型企业应用
岛屿架构极小(~5KB)极佳内容型网站、电商
SSR/SSG极佳博客、文档、营销页
RSC(React)极佳全栈应用

🎯 今日挑战

将一个传统 React SPA 重构为岛屿架构

  1. 识别页面中的静态部分和交互部分
  2. 将交互部分拆分为独立的岛屿组件
  3. 为每个岛屿设置合适的加载策略(client:load/visible/idle)
  4. 实现岛屿间的通信机制
  5. 对比重构前后的性能指标(LCP、FID、CLS)
---
// 重构示例: 电商商品详情页
// 重构前: 整个页面都是 React 组件(~200KB JS)
// 重构后: 只有购物车、评论、推荐是岛屿(~15KB JS)
---

<Layout>
  <!-- 静态内容:0KB JS -->
  <section class="product-gallery">
    <img src={product.image} alt={product.name} />
  </section>
  
  <section class="product-info">
    <h1>{product.name}</h1>
    <p>{product.description}</p>
    <div class="price">¥{product.price}</div>
  </section>
  
  <!-- 🏝️ 购物车岛屿:45KB JS(React) -->
  <Cart product={product} client:visible />
  
  <!-- 🏝️ 评论岛屿:30KB JS(Vue) -->
  <Comments productId={product.id} client:idle />
  
  <!-- 🏝️ 推荐岛屿:20KB JS(Svelte) -->
  <Recommendations productId={product.id} client:visible />
</Layout>

明日预告:全栈前端 - 使用 Next.js/Remix 构建完整应用

💡 架构箴言:"没有最好的架构,只有最合适的架构。岛屿架构是内容型网站的答案,但后台系统可能依然需要 SPA。"