Skip to content

在群岛之间共享状态

在使用 群岛架构/部分水化 搭建 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?

¥Why Nano Stores?

纳米存储 库允许你创作任何组件都可以与之交互的存储。我们推荐 Nano Store 的原因是:

¥The Nano Stores library allows you to author stores that any component can interact with. We recommend Nano Stores because:

  • 它们很轻。Nano Stores 提供了你所需的最小 JS(小于 1 KB)且零依赖。

  • 它们与框架无关。这意味着框架之间的状态共享将是无缝的!Astro 建立在灵活性之上,因此我们喜欢能够提供类似开发者体验的解决方案,无论你的偏好如何。

尽管如此,你仍然可以探索许多替代方案。这些包括:

¥Still, there are a number of alternatives you can explore. These include:

¥Installing Nano Stores

首先,为你最喜欢的 UI 框架安装 Nano Stores 及其辅助程序包:

¥To get started, install Nano Stores alongside their helper package for your favorite UI framework:

Terminal window
npm install nanostores @nanostores/preact

你可以从这里跳入 Nano 存储使用指南,或者按照我们下面的示例进行操作!

¥You can jump into the Nano Stores usage guide from here, or follow along with our example below!

使用示例 - 电子商务购物车弹出窗口

Section titled 使用示例 - 电子商务购物车弹出窗口

¥Usage example - ecommerce cart flyout

假设我们正在构建一个包含三个交互元素的简单电子商务界面:

¥Let’s say we’re building a simple ecommerce interface with three interactive elements:

  • “添加到购物车” 提交表格

  • 用于显示这些添加的项目的购物车弹出窗口

  • 购物车弹出开关

尝试完成的示例 在你的计算机上或通过 StackBlitz 在线。

¥*Try the completed example on your machine or online via StackBlitz.*

你的基本 Astro 文件可能如下所示:

¥Your base Astro file may look like this:

src/pages/index.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>

¥Using “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:

src/cartStore.js
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:

src/components/CartFlyoutToggle.jsx
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>
)
}

然后,我们可以从 CartFlyout 组件中读取 isCartOpen

¥Then, we can read isCartOpen from our CartFlyout component:

src/components/CartFlyout.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen } from '../cartStore';
export default function CartFlyout() {
const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;
}

¥Using “maps”

现在,让我们跟踪购物车内的商品。为了避免重复并跟踪 “数量,“,我们可以将你的购物车存储为一个对象,并以商品 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.

src/cartStore.js
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({});

现在,让我们导出一个 addCartItem 辅助程序供我们的组件使用。

¥Now, let’s export an addCartItem helper for our components to use.

  • 如果该商品不存在于你的购物车中,请添加起始数量为 1 的商品。

  • 如果该项目已经存在,则将数量增加 1。

src/cartStore.js
...
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 }
);
}
}

存储就位后,每当提交表单时,我们就可以在 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.

src/components/AddToCartForm.jsx
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>
)
}

最后,我们将在 CartFlyout 中渲染这些购物车项目:

¥Finally, we’ll render those cart items inside our CartFlyout:

src/components/CartFlyout.jsx
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;
}

现在,你应该有一个完全交互式的电子商务示例,其中包含银河系中最小的 JS 包 🚀

¥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!

Astro 中文网 - 粤ICP备13048890号