Chapter 05

集成其他框架:React/Vue/Svelte

在 Astro 中集成 React、Vue 和 Svelte 框架,掌握 client: 指令的用法与 Nano Stores 状态共享

多框架集成的设计理念

为什么 Astro 支持多框架?

Astro 的设计哲学是「UI 框架无关」。它不强迫你选择某个框架,而是让你在同一个项目中混合使用 React、Vue、Svelte 甚至 Preact。这在以下场景特别有用:

安装框架集成

# 使用 astro add 自动安装并配置(推荐)
npx astro add react
npx astro add vue
npx astro add svelte
npx astro add solid

# 或者手动安装
npm install @astrojs/react react react-dom
npm install @astrojs/vue vue
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';

export default defineConfig({
  integrations: [react(), vue(), svelte()],
});

在 Astro 中使用框架组件

React 组件

// src/components/Counter.jsx
import { useState } from 'react';

export default function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);

  return (
    <div className="counter">
      <button onClick={() => setCount(c => c - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

在 .astro 中使用,带 client: 指令

---
import Counter from '../components/Counter.jsx';
import SearchBox from '../components/SearchBox.jsx';
import VueModal from '../components/Modal.vue';
import SvelteAnimation from '../components/Anim.svelte';
---

<!-- 不加 client: → 服务端渲染,无JS,useState 不工作 -->
<Counter />

<!-- client:load → 立即激活,useState 工作 -->
<Counter client:load initialCount={5} />

<!-- client:visible → 滚动到视口才激活(节省初始 JS) -->
<SearchBox client:visible />

<!-- Vue 组件 -->
<VueModal client:idle />

<!-- Svelte 组件 -->
<SvelteAnimation client:load />

Nano Stores:跨框架状态共享

为什么需要 Nano Stores?

当你的页面有多个岛屿(一个 React 计数器、一个 Vue 购物车),它们可能需要共享状态。由于各岛屿是独立的 JS 运行时,不能共享 React/Vue 的响应式状态。Nano Stores 是一个极小的状态管理库(300 字节),专为多框架场景设计。

npm install nanostores @nanostores/react @nanostores/vue
// src/stores/cart.ts
import { atom, computed } from 'nanostores';

export const cartItems = atom<Array<{ id: string; name: string; qty: number }>>([]);

export const cartCount = computed(cartItems, items =>
  items.reduce((sum, item) => sum + item.qty, 0)
);

export function addToCart(item: { id: string; name: string }) {
  const current = cartItems.get();
  const existing = current.find(i => i.id === item.id);
  if (existing) {
    cartItems.set(current.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i));
  } else {
    cartItems.set([...current, { ...item, qty: 1 }]);
  }
}
// React 组件使用 Nano Store
import { useStore } from '@nanostores/react';
import { cartCount, addToCart } from '../stores/cart';

export default function AddButton({ product }) {
  const count = useStore(cartCount);
  return (
    <button onClick={() => addToCart(product)}>
      加入购物车(当前 {count} 件)
    </button>
  );
}
Nano Stores vs Redux vs Zustand

对于 Astro Islands 的跨框架状态,Nano Stores 是最佳选择:体积极小(不增加额外 JS),框架无关(React/Vue/Svelte/Solid 都有适配器),API 简单直观。Redux 等重量级状态库不适合岛屿架构。

client: 指令详解

client:load
页面加载完成后立即激活(hydrate)组件。适合关键交互组件(如导航菜单、登录按钮)。有最高优先级,会阻塞主线程。
client:idle
等待浏览器主线程空闲后激活(使用 requestIdleCallback)。适合非关键交互组件(如聊天小部件、推荐区域)。页面加载时不阻塞主线程。
client:visible
组件滚动进入视口时激活(使用 IntersectionObserver)。最节省 JS 资源:折叠区域下方的组件不会提前加载。适合评论区、相关推荐等下方内容。
client:media
满足 CSS 媒体查询条件时激活。如 client:media="(max-width: 768px)"——仅在移动端激活汉堡菜单。桌面端完全不加载该组件的 JS。
client:only="react"
跳过服务端渲染,仅在客户端渲染。适合依赖浏览器 API(localStorage、window)的组件,或无法在服务端运行的组件。需指定框架名。

Vue 组件集成示例

<!-- src/components/Dropdown.vue -->
<template>
  <div class="dropdown">
    <button @click="open = !open">{{ label }}</button>
    <ul v-if="open">
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
defineProps<{ label: string; items: Array<{ id: number; name: string }> }>()
const open = ref(false)
</script>
---
import Dropdown from '../components/Dropdown.vue';
const menuItems = [
  { id: 1, name: '首页' },
  { id: 2, name: '关于' },
];
---

<Dropdown client:idle label="菜单" items={menuItems} />

Server Islands:Astro 5 的延迟服务端渲染

Server Islands 是 Astro 5 引入的新特性,允许在静态页面中嵌入需要服务端动态渲染的组件——无需整个页面都开启 SSR。原理是:页面主体静态生成,标记了 server:defer 的组件在浏览器请求时异步获取其服务端渲染结果(通过独立的 HTTP 请求),然后替换占位符。

静态岛屿(Islands Architecture)
页面主体是静态 HTML(CDN 缓存),交互性组件用 client:* 指令在客户端激活。这是 Astro 的基础架构——零 JS by default。
Server Islands(服务端岛屿)
Astro 5 新增。标记 server:defer 的组件在请求时由服务端动态渲染(个性化内容、认证信息),而页面其余部分仍是静态缓存的 HTML。兼顾静态速度和动态内容的最佳方案。
fallback(后备内容)
Server Islands 在加载期间显示的占位内容(骨架屏、加载图标)。通过 slot="fallback" 定义。服务端组件渲染完成后,浏览器用真实内容替换占位符。
---
// src/pages/dashboard.astro
// 此页面可以是纯静态的!
import UserStats from '../components/UserStats.astro';
import RecommendedPosts from '../components/RecommendedPosts.astro';
---

<!-- 页面主体:静态生成,CDN 缓存 -->
<main>
  <h1>仪表盘</h1>

  <!-- Server Island:个性化内容,按需服务端渲染 -->
  <UserStats server:defer>
    <!-- fallback slot:渲染期间显示骨架屏 -->
    <div slot="fallback" class="skeleton">加载中...</div>
  </UserStats>

  <!-- 另一个 Server Island -->
  <RecommendedPosts server:defer>
    <div slot="fallback">
      {[1,2,3].map(() => <div class="skeleton-card"></div>)}
    </div>
  </RecommendedPosts>
</main>
---
// src/components/UserStats.astro
// 此组件在请求时服务端执行(每次用户访问)
import { getSession } from '../lib/auth';

const session = await getSession(Astro.request);
const stats = await db.user.getStats(session?.userId);
---

<!-- 这段 HTML 是动态渲染的,包含用户个性化数据 -->
<div class="stats-card">
  <p>欢迎回来,{session?.user.name}!</p>
  <p>您有 {stats.unreadMessages} 条未读消息</p>
</div>
Server Islands vs SSR 的选择

如果页面上只有少量动态内容(如用户名、购物车数量、个性化推荐),用 Server Islands 比整页 SSR 更高效:页面外壳走 CDN 缓存(毫秒级),动态部分独立请求。只有当大部分页面内容都需要动态化时,才应该开启整页 SSR。这是 Astro 5 性能优化的核心思路。

Server Islands 的前提条件

server:defer 要求项目配置了 SSR 适配器(Vercel、Netlify、Node 等)——即使页面本身是静态预渲染的(output: 'static' 或 output: 'hybrid'),也需要适配器来处理 Server Islands 的独立请求。如果没有安装适配器,使用 server:defer 会报构建错误。

本章小结

本章核心要点