在群岛之间共享状态
在使用 群岛架构/部分水化 搭建 Astro 网站时,你可能遇到过这样的问题: 我想在我的组件之间共享状态。
英When building an Astro website with islands architecture / partial hydration, you may have run into this problem: I want to share state between my components.
像 React 或 Vue 这样的 UI 框架可能会鼓励其他组件使用 “context” 提供商。 但是当 部分混合组件 在 Astro 或 Markdown 中时,你不能使用这些上下文封装器。
英UI frameworks like React or Vue may encourage “context” providers for other components to consume. But when partially hydrating components within Astro or Markdown, you can’t use these context wrappers.
Astro 建议采用不同的共享客户端存储解决方案: 纳米存储。
英Astro recommends a different solution for shared client-side storage: Nano Stores.

为什么选择 Nano Stores?
Section titled 为什么选择 Nano Stores?纳米存储 库允许你创作任何组件都可以与之交互的存储。 我们推荐 Nano Store 的原因是:
- 它们很轻。 Nano Stores 提供了你所需的最小 JS(小于 1 KB)且零依赖。
- 它们与框架无关。 这意味着框架之间的状态共享将是无缝的! Astro 建立在灵活性之上,因此我们喜欢能够提供类似开发者体验的解决方案,无论你的偏好如何。
尽管如此,你仍然可以探索许多替代方案。 这些包括:
- Svelte 的内置存储
- 稳定的信号 在组件上下文之外
- Vue 的反应性 API
- 发送自定义浏览器事件 组件之间
安装 Nano 存储
Section titled 安装 Nano 存储首先,为你最喜欢的 UI 框架安装 Nano Stores 及其辅助程序包:
英To get started, install Nano Stores alongside their helper package for your favorite UI framework:
npm install nanostores @nanostores/preact
npm install nanostores @nanostores/react
npm install nanostores @nanostores/solid
npm install nanostores
:::注意 这里没有帮助包! Nano Stores 可以像标准 Svelte 存储一样使用。 :::
npm install nanostores @nanostores/vue
npm install nanostores @nanostores/lit
你可以从这里跳入 Nano 存储使用指南,或者按照我们下面的示例进行操作!
英You can jump into the Nano Stores usage guide from here, or follow along with our example below!
用法示例 - 电子商务购物车弹出窗口
Section titled 用法示例 - 电子商务购物车弹出窗口假设我们正在构建一个包含三个交互元素的简单电子商务界面:
- “添加到购物车” 提交表格
- 用于显示这些添加的项目的购物车弹出窗口
- 购物车弹出开关
尝试完成的示例 在你的计算机上或通过 StackBlitz 在线。
英Try the completed example on your machine or online via StackBlitz.
你的基本 Astro 文件可能如下所示:
英Your base Astro file may look like this:
---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”
Section titled 使用 “atoms”让我们首先在单击 CartFlyoutToggle
时打开 CartFlyout
。
英Let’s start by opening our CartFlyout
whenever CartFlyoutToggle
is clicked.
首先,创建一个新的 JS 或 TS 文件来包含我们的存储。 为此,我们将使用 “atom”:
英First, create a new JS or TS file to contain our store. We’ll use an “atom” for this:
import { atom } from 'nanostores';
export const isCartOpen = atom(false);
现在,我们可以将此存储导入到任何需要读取或写入的文件中。 我们首先连接 CartFlyoutToggle
:
英Now, we can import this store into any file that needs to read or write. We’ll start by wiring up our 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>
import { LitElement, html } from 'lit';import { isCartOpen } from '../cartStore';
export class CartFlyoutToggle extends LitElement { handleClick() { isCartOpen.set(!isCartOpen.get()); }
render() { return html` <button @click="${this.handleClick}">Cart</button> `; }}
customElements.define('cart-flyout-toggle', CartFlyoutToggle);
然后,我们可以从 CartFlyout
组件中读取 isCartOpen
:
英Then, we can read isCartOpen
from our CartFlyout
component:
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>
import { isCartOpen } from '../cartStore';import { LitElement, html } from 'lit';import { StoreController } from '@nanostores/lit';
export class CartFlyout extends LitElement { private cartOpen = new StoreController(this, isCartOpen);
render() { return this.cartOpen.value ? html`<aside>...</aside>` : null; }}
customElements.define('cart-flyout', CartFlyout);
使用 “maps”
Section titled 使用 “maps”:::tip 提示
对于你经常写入的对象来说,地图 是一个不错的选择! 除了 atom
提供的标准 get()
和 set()
辅助程序之外,你还将拥有 .setKey()
函数来有效更新单个对象键。
:::
现在,让我们跟踪购物车内的商品。 为了避免重复并跟踪 “数量,“,我们可以将你的购物车存储为一个对象,并以商品 ID 作为键。 为此,我们将使用 地图。
英Now, let’s keep track of the items inside your cart. To avoid duplicates and keep track of “quantity,” we can store your cart as an object with the item’s ID as a key. We’ll use a Map for this.
让我们将 cartItem
存储添加到之前的 cartStore.js
中。 如果你愿意,还可以切换到 TypeScript 文件来定义形状。
英Let’s add a cartItem
store to our cartStore.js
from earlier. You can also switch to a TypeScript file to define the shape if you’re so inclined.
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 } ); }}
:::note 注意
🙋 为什么在这里使用 .get()
而不是 useStore
助手?
你可能已经注意到我们在这里调用 cartItems.get()
,而不是从我们的 React / Preact / Solid / Vue 示例中获取 useStore
辅助程序。 这是因为 useStore 旨在触发组件重新渲染。 换句话说,只要将存储值渲染到 UI,就应该使用 useStore
。 由于我们在触发 event 时读取该值(本例中为 addToCart
),并且我们不尝试渲染该值,因此这里不需要 useStore
。
:::
存储就位后,每当提交表单时,我们就可以在 AddToCartForm
内调用此函数。 我们还将打开购物车弹出窗口,以便你可以看到完整的购物车摘要。
英With our store in place, we can call this function inside our AddToCartForm
whenever that form is submitted. We’ll also open the cart flyout so you can see a full cart summary.
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>
import { LitElement, html } from 'lit';import { isCartOpen, addCartItem } from '../cartStore';
export class AddToCartForm extends LitElement { static get properties() { return { item: { type: Object }, }; }
constructor() { super(); this.item = {}; }
addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(this.item); }
render() { return html` <form @submit="${this.addToCart}"> <slot></slot> </form> `; }}customElements.define('add-to-cart-form', AddToCartForm);
最后,我们将在 CartFlyout
中渲染这些购物车项目:
英Finally, we’ll render those cart items inside our 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>
import { LitElement, html } from 'lit';import { isCartOpen, cartItems } from '../cartStore';import { StoreController } from '@nanostores/lit';
export class CartFlyoutLit extends LitElement { private cartOpen = new StoreController(this, isCartOpen); private getCartItems = new StoreController(this, cartItems);
renderCartItem(cartItem) { return html` <li> <img src="${cartItem.imageSrc}" alt="${cartItem.name}" /> <h3>${cartItem.name}</h3> <p>Quantity: ${cartItem.quantity}</p> </li> `; }
render() { return this.cartOpen.value ? html` <aside> ${ Object.values(this.getCartItems.value).length ? html` <ul> ${Object.values(this.getCartItems.value).map((cartItem) => this.renderCartItem(cartItem) )} </ul> ` : html`<p>Your cart is empty!</p>` } </aside> ` : null; }}
customElements.define('cart-flyout', CartFlyoutLit);
Now, you should have a fully interactive ecommerce example with the smallest JS bundle in the galaxy 🚀
英Now, you should have a fully interactive ecommerce example with the smallest JS bundle in the galaxy 🚀
尝试完成的示例 在你的机器上或通过 StackBlitz 在线!
英Try the completed example on your machine or online via StackBlitz!
Learn