在群岛之间共享状态
在使用 岛屿架构/部分水合 构建 Astro 网站时,你可能会遇到这个问题:我希望在我的组件之间共享状态。
像 React 或 Vue 这样的 UI 框架可能会鼓励使用 “context” 提供者 供其他组件消费。但是在 Astro 或 Markdown 中 部分水合组件 时,你不能使用这些 context 包装器。
Astro 推荐一种不同的共享客户端存储解决方案:Nano Stores。

为什么选择 Nano Stores?
标题为 “为什么选择 Nano Stores?” 的章节Nano Stores 库允许你创建任何组件都可以与之交互的 store。我们推荐 Nano Stores 是因为
- 它们是轻量级的。 Nano Stores 只提供你所需的最少的 JS(小于 1 KB),且无任何依赖。
- 它们是框架无关的。 这意味着在不同框架之间共享状态将是无缝的!Astro 建立在灵活性之上,所以我们喜欢那些无论你的偏好如何都能提供相似开发者体验的解决方案。
不过,你也可以探索一些其他的替代方案。这些包括
- Svelte 的内置 stores
- 在组件上下文之外使用 Solid signals
- Vue 的响应式 API
- 在组件之间发送自定义浏览器事件
🙋 我可以在 .astro
文件或其他服务器端组件中使用 Nano Stores 吗?
Nano Stores 可以在 <script>
标签中使用,以在 .astro
组件之间共享状态。但是,不建议在服务器端组件的 frontmatter 中使用 Nano Stores,因为有以下限制
- 从
.astro
文件或非水合组件写入 store *不会*影响客户端组件接收到的值。 - 你不能将 Nano Store 作为 “prop” 传递给客户端组件。
- 你不能从
.astro
文件中订阅 store 的变化,因为 Astro 组件不会重新渲染。
如果你理解这些限制并且仍然找到了使用场景,你可以尝试使用 Nano Stores!但请记住,Nano Stores 是专门为响应客户端上的变化而构建的。
🙋 Svelte stores 和 Nano Stores 相比如何?
Nano Stores 和 Svelte stores 非常相似! 事实上,nanostores 允许你使用与 Svelte stores 相同的 $
快捷方式进行订阅。
如果你想避免使用第三方库,Svelte stores 本身就是一个很好的跨岛屿通信工具。不过,如果你 a) 喜欢它们用于“对象”和异步状态的附加组件,或者 b) 你想在 Svelte 和其他 UI 框架(如 Preact 或 Vue)之间进行通信,你可能会更喜欢 Nano Stores。
🙋 Solid signals 和 Nano Stores 相比如何?
如果你已经使用 Solid 一段时间,你可能已经尝试过将 signals 或 stores 移出组件。这是在 Solid 岛屿之间共享状态的好方法!可以尝试从一个共享文件中导出 signals
import { createSignal } from 'solid-js';
export const sharedCount = createSignal(0);
……所有导入 sharedCount
的组件都将共享相同的状态。虽然这很有效,但如果你 a) 喜欢它们用于“对象”和异步状态的附加组件,或者 b) 你想在 Solid 和其他 UI 框架(如 Preact 或 Vue)之间进行通信,你可能会更喜欢 Nano Stores。
安装 Nano Stores
标题为 “安装 Nano Stores” 的章节要开始使用,请安装 Nano Stores 以及你最喜欢的 UI 框架的辅助包
npm install nanostores @nanostores/preact
npm install nanostores @nanostores/react
npm install nanostores @nanostores/solid
npm install nanostores
这里不需要辅助包!Nano Stores 可以像标准的 Svelte stores 一样使用。
npm install nanostores @nanostores/vue
你可以从这里直接跳到 Nano Stores 使用指南,或者跟随我们下面的示例!
使用示例 - 电商购物车浮出
标题为 “使用示例 - 电商购物车浮出” 的章节假设我们正在构建一个简单的电商界面,包含三个交互元素
- 一个“添加到购物车”的提交表单
- 一个用于显示已添加商品的购物车浮出层
- 一个购物车浮出层切换器
尝试完整示例,可在你的本地计算机上或通过 StackBlitz 在线运行。
你的基础 Astro 文件可能看起来像这样
---import CartFlyoutToggle from '../components/CartFlyoutToggle';import CartFlyout from '../components/CartFlyout';import AddToCartForm from '../components/AddToCartForm';---
<!DOCTYPE html><html lang="en"><head>...</head><body> <header> <nav> <a href="/">Astro storefront</a> <CartFlyoutToggle client:load /> </nav> </header> <main> <AddToCartForm client:load> <!-- ... --> </AddToCartForm> </main> <CartFlyout client:load /></body></html>
使用 “atoms”
标题为 “使用 “atoms”” 的章节让我们从点击 CartFlyoutToggle
时打开 CartFlyout
开始。
首先,创建一个新的 JS 或 TS 文件来存放我们的 store。我们将为此使用一个 “atom”
import { atom } from 'nanostores';
export const isCartOpen = atom(false);
现在,我们可以将这个 store 导入任何需要读或写的文件中。我们先来连接我们的 CartFlyoutToggle
import { useStore } from '@nanostores/preact';import { isCartOpen } from '../cartStore';
export default function CartButton() { // read the store value with the `useStore` hook const $isCartOpen = useStore(isCartOpen); // write to the imported store using `.set` return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button> )}
import { useStore } from '@nanostores/react';import { isCartOpen } from '../cartStore';
export default function CartButton() { // read the store value with the `useStore` hook const $isCartOpen = useStore(isCartOpen); // write to the imported store using `.set` return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button> )}
import { useStore } from '@nanostores/solid';import { isCartOpen } from '../cartStore';
export default function CartButton() { // read the store value with the `useStore` hook const $isCartOpen = useStore(isCartOpen); // write to the imported store using `.set` return ( <button onClick={() => isCartOpen.set(!$isCartOpen())}>Cart</button> )}
<script> import { isCartOpen } from '../cartStore';</script>
<!--use "$" to read the store value--><button on:click={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
<template> <!--write to the imported store using `.set`--> <button @click="isCartOpen.set(!$isCartOpen)">Cart</button></template>
<script setup> import { isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
// read the store value with the `useStore` hook const $isCartOpen = useStore(isCartOpen);</script>
然后,我们可以从我们的 CartFlyout
组件中读取 isCartOpen
import { useStore } from '@nanostores/preact';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;}
import { useStore } from '@nanostores/react';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;}
import { useStore } from '@nanostores/solid';import { isCartOpen } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);
return $isCartOpen() ? <aside>...</aside> : null;}
<script> import { isCartOpen } from '../cartStore';</script>
{#if $isCartOpen}<aside>...</aside>{/if}
<template> <aside v-if="$isCartOpen">...</aside></template>
<script setup> import { isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
const $isCartOpen = useStore(isCartOpen);</script>
使用 “maps”
标题为 “使用 “maps”” 的章节对于你经常写入的对象,Maps 是一个很好的选择! 除了 atom
提供的标准 get()
和 set()
辅助函数外,你还会有一个 .setKey()
函数来高效地更新单个对象键。
现在,让我们来跟踪购物车中的商品。为了避免重复并跟踪“数量”,我们可以将购物车存储为一个以商品 ID 为键的对象。我们将为此使用一个 Map。
让我们将一个 cartItem
store 添加到我们之前的 cartStore.js
中。如果你愿意,也可以切换到 TypeScript 文件来定义其类型。
import { atom, map } from 'nanostores';
export const isCartOpen = atom(false);
/** * @typedef {Object} CartItem * @property {string} id * @property {string} name * @property {string} imageSrc * @property {number} quantity */
/** @type {import('nanostores').MapStore<Record<string, CartItem>>} */export const cartItems = map({});
import { atom, map } from 'nanostores';
export const isCartOpen = atom(false);
export type CartItem = { id: string; name: string; imageSrc: string; quantity: number;}
export const cartItems = map<Record<string, CartItem>>({});
现在,让我们导出一个 addCartItem
辅助函数供我们的组件使用。
- 如果该商品在你的购物车中不存在,则添加该商品,并将初始数量设置为 1。
- 如果该商品确实已经存在,则将其数量增加 1。
...export function addCartItem({ id, name, imageSrc }) { const existingEntry = cartItems.get()[id]; if (existingEntry) { cartItems.setKey(id, { ...existingEntry, quantity: existingEntry.quantity + 1, }) } else { cartItems.setKey( id, { id, name, imageSrc, quantity: 1 } ); }}
...type ItemDisplayInfo = Pick<CartItem, 'id' | 'name' | 'imageSrc'>;export function addCartItem({ id, name, imageSrc }: ItemDisplayInfo) { const existingEntry = cartItems.get()[id]; if (existingEntry) { cartItems.setKey(id, { ...existingEntry, quantity: existingEntry.quantity + 1, }); } else { cartItems.setKey( id, { id, name, imageSrc, quantity: 1 } ); }}
🙋 为什么在这里使用 .get()
而不是 useStore
辅助函数?
你可能已经注意到我们在这里调用了 cartItems.get()
,而不是像在 React / Preact / Solid / Vue 示例中那样使用 useStore
辅助函数。这是因为 useStore 是用来触发组件重新渲染的。 换句话说,每当 store 的值需要被渲染到 UI 上时,就应该使用 useStore
。由于我们是在触发事件(本例中是 addToCart
)时读取值,并且我们不打算渲染这个值,所以这里不需要使用 useStore
。
有了我们的 store,我们就可以在 AddToCartForm
表单提交时调用这个函数。我们还会打开购物车浮出层,以便你能看到完整的购物车摘要。
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // we'll hardcode the item info for simplicity! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // we'll hardcode the item info for simplicity! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) { // we'll hardcode the item info for simplicity! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }
return ( <form onSubmit={addToCart}> {children} </form> )}
<form on:submit|preventDefault={addToCart}> <slot></slot></form>
<script> import { addCartItem, isCartOpen } from '../cartStore';
// we'll hardcode the item info for simplicity! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart() { isCartOpen.set(true); addCartItem(hardcodedItemInfo); }</script>
<template> <form @submit="addToCart"> <slot></slot> </form></template>
<script setup> import { addCartItem, isCartOpen } from '../cartStore';
// we'll hardcode the item info for simplicity! const hardcodedItemInfo = { id: 'astronaut-figurine', name: 'Astronaut Figurine', imageSrc: '/images/astronaut-figurine.png', }
function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }</script>
最后,我们将在 CartFlyout
中渲染这些购物车商品
import { useStore } from '@nanostores/preact';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen ? ( <aside> {Object.values($cartItems).length ? ( <ul> {Object.values($cartItems).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}
import { useStore } from '@nanostores/react';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen ? ( <aside> {Object.values($cartItems).length ? ( <ul> {Object.values($cartItems).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}
import { useStore } from '@nanostores/solid';import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);
return $isCartOpen() ? ( <aside> {Object.values($cartItems()).length ? ( <ul> {Object.values($cartItems()).map(cartItem => ( <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> ))} </ul> ) : <p>Your cart is empty!</p>} </aside> ) : null;}
<script> import { isCartOpen, cartItems } from '../cartStore';</script>
{#if $isCartOpen} {#if Object.values($cartItems).length} <aside> {#each Object.values($cartItems) as cartItem} <li> <img src={cartItem.imageSrc} alt={cartItem.name} /> <h3>{cartItem.name}</h3> <p>Quantity: {cartItem.quantity}</p> </li> {/each} </aside> {:else} <p>Your cart is empty!</p> {/if}{/if}
<template> <aside v-if="$isCartOpen"> <ul v-if="Object.values($cartItems).length"> <li v-for="cartItem in Object.values($cartItems)" v-bind:key="cartItem.name"> <img :src=cartItem.imageSrc :alt=cartItem.name /> <h3>{{cartItem.name}}</h3> <p>Quantity: {{cartItem.quantity}}</p> </li> </ul> <p v-else>Your cart is empty!</p> </aside></template>
<script setup> import { cartItems, isCartOpen } from '../cartStore'; import { useStore } from '@nanostores/vue';
const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);</script>
现在,你应该拥有一个功能完备的交互式电商示例,它拥有全宇宙最小的 JS 包 🚀
尝试完整示例,可在你的本地计算机上或通过 StackBlitz 在线运行!
常用方案