状态共享

当我们使用 群岛结构 / 部分激活,你可能会遇到这样的问题:我想在我的组件之间共享状态。

像 Vue 或者 React 这样的UI框架可能会鼓励使用 “上下文” 提供者(“context” provider) 来为其他组件提供上下文信息。 但是在 Astro 或者 Markdown 中的 部分激活组件(partially hydrating components) 不能使用上下文封装。

Astro 推荐了一个不同的客户端共享存储的解决方案: Nano Stores

Nano Stores 库允许你编写任何组件都能与之互动的状态库。我们推荐Nano Stores,因为:

  • 它是轻量级的。 Nano Stores 提供了你所需要的最低限度的JS(不到1KB),并且没有依赖性。
  • 它是框架无关的。 这意味着在 Preact、Svelte 和 Vue 之间共享状态将是无缝的! Astro 是建立在灵活性之上的,所以我们喜欢那些无论你的偏好如何都能提供类似开发者体验的解决方案。

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

为你喜欢的 UI 框架安装 Nano Stores 和他们的帮助包:

npm i nanostores @nanostores/preact

你可以跳转到 Nano Stores 使用指南 或者跟随我们下面的例子!

用例 - 电商购物车抽屉

Section titled 用例 - 电商购物车抽屉

假如我们正在搭建一个简单的电商页面,有下面三个交互元素:

  • 一个 “add to cart” 按钮
  • 一个购物车抽屉来显示已添加的商品
  • 一个购物车抽屉开关

克隆仓库以便在你的机器上尝试已完成的例子或者通过 Stackblitz 在线尝试

你基础的 Astro 文件看起来应该是这样的:

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

首先,创建一个新的 JS 或 TS 文件来存放我们的状态库。我们将会使用 “atom” 来做这件事:

src/cartStore.js
import { atom } from 'nanostores';

export const isCartOpen = atom(false);

现在,我们可以在任意文件中导入这个状态库来进行读写。我们接下来着手开发我们的 CartFlyoutToggle 组件:

src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen } from '../cartStore';

export default function CartButton() {
  // 使用 `useStore` 钩子来读取状态库
  const $isCartOpen = useStore(isCartOpen);
  // 使用 `.set` 来将数据写入状态库
  return (
    <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
  )
}

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

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

现在,让我们来跟踪你购物车里的商品。为了避免重复和跟踪 “数量”,我们可以把你的购物车存储为一个对象,以商品的ID为键。我们将使用一个 Map 来做这件事。

让我们在先前的 cartStore.js 中添加一个 cartItem 状态库。如果你愿意的话,你也可以使用 TypeScript 文件来定义。

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

有了状态库之后,我们就可以在每次提交表单时调用 AddToCartForm函数。我们还可以打开购物车抽屉,这样你就可以看到一个完整的购物车概要。

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 组件中渲染购物车商品:

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 包 🚀

在你的机器上尝试完整的例子 或者通过 Stackblitz 在线尝试!