服务端阅读 05月27日 15:55
Astro 中的状态管理是如何实现的?如何在 React、Vue、Svelte 组件中管理状态?
Astro 的状态管理与传统 SPA 框架有本质区别。理解 Astro 的岛屿架构(Island Architecture)是掌握其状态管理的关键前提。岛屿架构如何影响状态管理Astro 默认发送零 JavaScript,页面以纯静态 HTML 输出。交互组件需要通过 client:* 指令才会被水合(hydrate)并获得客户端状态管理能力。这意味着:没有 client:* 指令的组件只渲染静态 HTML,点击按钮不会触发状态更新每个岛屿(island)是独立的,拥有各自的生命周期和状态作用域岛屿之间默认无法共享状态,这是 Astro 状态管理的核心挑战Astro 提供五种水合指令控制组件何时激活:client:load:页面加载后立即水合,适合首屏关键交互client:idle:浏览器空闲时水合,适合低优先级组件client:visible:组件进入视口时水合,适合首屏以下内容client:media:匹配媒体查询时水合,适合响应式场景client:only:跳过服务端渲染,仅在客户端渲染各框架组件的局部状态管理加上 client:* 指令后,各框架组件使用各自的内置状态机制,写法与在原生框架中完全一致。React 组件React 组件使用 useState 和 useEffect 管理状态:// src/components/Counter.jsximport { useState } from 'react';export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>当前计数: {count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> <button onClick={() => setCount(c => c - 1)}>-1</button> </div> );}在 Astro 页面中使用时必须加上 client:* 指令:---import Counter from '../components/Counter.jsx';---<Counter client:load />如果漏掉 client:load,组件只会渲染初始 HTML,按钮点击不会有任何响应。Vue 组件Vue 组件使用 ref 和 computed 管理状态:<!-- src/components/TodoList.vue --><script setup>import { ref, computed } from 'vue';const todos = ref([ { id: 1, text: '学习 Astro', done: false }, { id: 2, text: '构建应用', done: true },]);const remaining = computed(() => todos.value.filter(t => !t.done).length);function toggle(id) { const todo = todos.value.find(t => t.id === id); if (todo) todo.done = !todo.done;}</script><template> <p>剩余 {{ remaining }} 项</p> <ul> <li v-for="todo in todos" :key="todo.id"> <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" /> <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }"> {{ todo.text }} </span> </li> </ul></template>在 Astro 页面中使用:---import TodoList from '../components/TodoList.vue';---<TodoList client:visible />Svelte 组件Svelte 使用内置 store 管理状态,语法最简洁:<!-- src/components/Cart.svelte --><script> import { writable } from 'svelte/store'; const items = writable([]); const total = writable(0); function add(product) { items.update(list => [...list, product]); total.update(t => t + product.price); }</script><h2>购物车</h2><p>合计: {$total} 元</p>{#each $items as item} <p>{item.name} - {item.price} 元</p>{/each}跨框架状态共享:Nanostores各框架的局部状态(React 的 useState、Vue 的 ref、Svelte 的 writable)只能在同一个岛屿内部使用。当不同框架的岛屿需要共享状态时,Astro 官方推荐使用 Nanostores。Nanostores 是一个不到 1KB 的框架无关状态管理库,为每个框架提供适配器,实现跨岛屿状态同步。安装npm install nanostores @nanostores/react @nanostores/vue# Svelte 无需额外包,原生支持创建共享 Store// src/store/cartStore.jsimport { atom, map } from 'nanostores';export const isCartOpen = atom(false);export const cartItems = map({});export function addCartItem(name, price) { cartItems.set({ ...cartItems.get(), [name]: { name, price } }); isCartOpen.set(true);}React 岛屿读取共享状态// src/components/CartButton.jsximport { useStore } from '@nanostores/react';import { isCartOpen, cartItems } from '../store/cartStore';export default function CartButton() { const $isOpen = useStore(isCartOpen); const $items = useStore(cartItems); const count = Object.keys($items).length; return ( <button onClick={() => isCartOpen.set(!$isOpen)}> 购物车 ({count}) </button> );}Vue 岛屿读取共享状态<!-- src/components/CartPanel.vue --><script setup>import { useStore } from '@nanostores/vue';import { isCartOpen, cartItems } from '../store/cartStore';const $isOpen = useStore(isCartOpen);const $items = useStore(cartItems);</script><template> <div v-if="$isOpen" class="cart-panel"> <h3>购物车</h3> <ul> <li v-for="(item, key) in $items" :key="key"> {{ item.name }} - {{ item.price }} 元 </li> </ul> </div></template>Svelte 岛屿读取共享状态<!-- src/components/CartCount.svelte --><script> import { isCartOpen, cartItems } from '../store/cartStore';</script><span>商品数: {Object.keys($cartItems).length}</span><button on:click={() => isCartOpen.set(!$isCartOpen)}> {#if $isCartOpen}关闭{:else}打开{/if}</button>在 Astro 页面中组合---import CartButton from '../components/CartButton.jsx';import CartPanel from '../components/CartPanel.vue';import CartCount from '../components/CartCount.svelte';---<header> <CartButton client:load /> <CartCount client:load /></header><main> <CartPanel client:load /></main>三个不同框架的岛屿通过 Nanostores 共享同一份购物车状态,修改一个岛屿中的数据会实时反映到其他岛屿。全局状态管理方案选择除了 Nanostores,Astro 项目还可以根据场景选择其他方案:Zustand(React 专属项目)如果项目只使用 React,Zustand 是更熟悉的选择:// src/store/useStore.tsimport { create } from 'zustand';interface AppState { user: { name: string } | null; theme: 'light' | 'dark'; setUser: (user: { name: string }) => void; setTheme: (theme: 'light' | 'dark') => void;}export const useStore = create<AppState>((set) => ({ user: null, theme: 'light', setUser: (user) => set({ user }), setTheme: (theme) => set({ theme }),}));Zustand 的状态只在 React 岛屿内有效,无法直接被 Vue 或 Svelte 岛屿访问。React Query / TanStack Query(服务端数据缓存)用于管理从 API 获取的服务端状态,自动处理缓存、重试和失效更新:// src/components/PostList.jsximport { useQuery } from '@tanstack/react-query';export default function PostList() { const { data, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), }); if (isLoading) return <p>加载中...</p>; if (error) return <p>加载失败</p>; return ( <ul> {data.map(post => <li key={post.id}>{post.title}</li>)} </ul> );}服务端数据获取Astro 组件在服务端执行,可以直接在 frontmatter 中获取数据,无需客户端请求:---// src/pages/dashboard.astroconst user = await getUser(Astro.request);const posts = await getCollection('blog');---<h1>{user.name} 的仪表板</h1><ul> {posts.map(post => <li>{post.data.title}</li>)}</ul>这种方式获取的数据作为静态 HTML 输出,不产生客户端 JavaScript 开销,是 Astro 性能优势的核心来源。URL 作为状态源筛选、分页、排序等状态适合存储在 URL 参数中,好处是可分享、可前进后退:// src/components/ProductFilter.jsximport { useSearchParams } from 'react-router-dom';export default function ProductFilter() { const [params, setParams] = useSearchParams(); const category = params.get('category') || 'all'; const page = parseInt(params.get('page') || '1'); function update(key, value) { setParams(prev => { const next = new URLSearchParams(prev); next.set(key, value); return next; }); } return ( <div> <select value={category} onChange={e => update('category', e.target.value)}> <option value="all">全部分类</option> <option value="electronics">电子产品</option> </select> <button disabled={page === 1} onClick={() => update('page', page - 1)}>上一页</button> <span>第 {page} 页</span> <button onClick={() => update('page', page + 1)}>下一页</button> </div> );}状态管理方案选型指南| 场景 | 推荐方案 | 说明 ||------|---------|------|| 单岛屿内部状态 | 框架内置(useState/ref/writable) | 最简单,无额外依赖 || 跨岛屿/跨框架共享 | Nanostores | 官方推荐,轻量通用 || 仅 React 项目全局状态 | Zustand | API 简洁,社区活跃 || API 数据缓存 | TanStack Query | 自动缓存和失效更新 || 筛选/分页/排序 | URL 参数 | 可分享,支持前进后退 || 静态内容展示 | Astro frontmatter 直接获取 | 零 JS 开销 |选择状态管理方案时,核心判断依据是状态的共享范围:岛屿内部用框架内置方案,跨岛屿用 Nanostores,服务端数据优先在 Astro frontmatter 中直接获取。