跳转到内容

在群岛之间共享状态

在使用 岛屿架构/部分水合 构建 Astro 网站时,你可能会遇到这个问题:我希望在我的组件之间共享状态。

像 React 或 Vue 这样的 UI 框架可能会鼓励使用 “context” 提供者 供其他组件消费。但是在 Astro 或 Markdown 中 部分水合组件 时,你不能使用这些 context 包装器。

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

Nano Stores 库允许你创建任何组件都可以与之交互的 store。我们推荐 Nano Stores 是因为

  • 它们是轻量级的。 Nano Stores 只提供你所需的最少的 JS(小于 1 KB),且无任何依赖。
  • 它们是框架无关的。 这意味着在不同框架之间共享状态将是无缝的!Astro 建立在灵活性之上,所以我们喜欢那些无论你的偏好如何都能提供相似开发者体验的解决方案。

不过,你也可以探索一些其他的替代方案。这些包括

要开始使用,请安装 Nano Stores 以及你最喜欢的 UI 框架的辅助包

终端窗口
npm install nanostores @nanostores/preact

你可以从这里直接跳到 Nano Stores 使用指南,或者跟随我们下面的示例!

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

  • 一个“添加到购物车”的提交表单
  • 一个用于显示已添加商品的购物车浮出层
  • 一个购物车浮出层切换器

尝试完整示例,可在你的本地计算机上或通过 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 文件来存放我们的 store。我们将为此使用一个 “atom”

src/cartStore.js
import { atom } from 'nanostores';
export const isCartOpen = atom(false);

现在,我们可以将这个 store 导入任何需要读或写的文件中。我们先来连接我们的 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

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

让我们将一个 cartItem store 添加到我们之前的 cartStore.js 中。如果你愿意,也可以切换到 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 }
);
}
}

有了我们的 store,我们就可以在 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 在线运行!

贡献 社区 赞助