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?

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

  • 它们很轻。 Nano Stores 提供了你所需的最小 JS(小于 1 KB)且零依赖。
  • 它们与框架无关。 这意味着框架之间的状态共享将是无缝的! Astro 建立在灵活性之上,因此我们喜欢能够提供类似开发者体验的解决方案,无论你的偏好如何。

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

首先,为你最喜欢的 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 用法示例 - 电子商务购物车弹出窗口

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

  • “添加到购物车” 提交表格
  • 用于显示这些添加的项目的购物车弹出窗口
  • 购物车弹出开关

尝试完成的示例 在你的计算机上或通过 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>

让我们首先在单击 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;
}

:::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.

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 辅助程序供我们的组件使用。

  • 如果你的购物车中不存在该商品,添加起始数量为 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 }
);
}
}

:::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.

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;
}

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!