返回介绍

状态共享

发布于 2024-06-05 21:19:56 字数 23817 浏览 0 评论 0 收藏 0

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

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

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

相关操作指南: 在 Astro 组件中共享状态

为什么使用 Nano Stores?

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

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

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

安装 Nano Stores

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

  • Preact
  • React
  • Solid
  • Svelte
  • Vue
  • Lit
npm i nanostores @nanostores/preact
npm i nanostores @nanostores/react
npm i nanostores @nanostores/solid
npm i nanostores
npm i nanostores @nanostores/vue
npm install nanostores @nanostores/lit

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

用例 - 电商购物车抽屉

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

  • 一个 “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>

使用 “atoms”

让我们在点击购物车抽屉开关(CartFlyoutToggle)的时候打开购物车抽屉(CartFlyout

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

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


export const isCartOpen = atom(false);

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

  • Preact
  • React
  • Solid
  • Svelte
  • Vue
  • Lit
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>
  )
}
)}"> src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen } from '../cartStore';


export default function CartButton() {
  // 使用 `useStore` 钩子来读取状态库
  const $isCartOpen = useStore(isCartOpen);
  // 使用 `.set` 来将数据写入状态库
  return (
    <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
  )
}
)}"> src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/solid';
import { isCartOpen } from '../cartStore';


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


<!--使用 "$" 来读取状态库的值-->
<button on:click={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
"> src/components/CartFlyoutToggle.vue
<template>
  <!--使用 `.set` 来将数据写入状态库-->
  <button @click="isCartOpen.set(!$isCartOpen)">Cart</button>
</template>


<script setup>
  import { isCartOpen } from '../cartStore';
  import { useStore } from '@nanostores/vue';


  // 使用 `useStore` 钩子来读取状态库
  const $isCartOpen = useStore(isCartOpen);
</script>
"> src/components/CartFlyoutToggle.ts
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);
`; }}customElements.define('cart-flyout-toggle', CartFlyoutToggle);">

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

  • Preact
  • React
  • Solid
  • Svelte
  • Vue
  • Lit
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;
}
src/components/CartFlyout.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen } from '../cartStore';


export default function CartFlyout() {
  const $isCartOpen = useStore(isCartOpen);


  return $isCartOpen ? <aside>...</aside> : null;
}
src/components/CartFlyout.jsx
import { useStore } from '@nanostores/solid';
import { isCartOpen } from '../cartStore';


export default function CartFlyout() {
  const $isCartOpen = useStore(isCartOpen);


  return $isCartOpen ? <aside>...</aside> : null;
}
src/components/CartFlyout.svelte
<script>
  import { isCartOpen } from '../cartStore';
</script>


{#if $isCartOpen}
<aside>...</aside>
{/if}
src/components/CartFlyout.vue
<template>
  <aside v-if="$isCartOpen">...</aside>
</template>


<script setup>
  import { isCartOpen } from '../cartStore';
  import { useStore } from '@nanostores/vue';


  const $isCartOpen = useStore(isCartOpen);
</script>
src/components/CartFlyout.ts
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”

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

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

  • JavaScript
  • 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({});
src/cartStore.ts
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。
  • JavaScript
  • TypeScript
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 }
    );
  }
}
src/cartStore.ts
...
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 }
    );
  }
}

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

  • Preact
  • React
  • Solid
  • Svelte
  • Vue
  • Lit
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>
  )
}
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>
  )
}
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>
  )
}
src/components/AddToCartForm.svelte
<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>
src/components/AddToCartForm.vue
<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>
src/components/AddToCartForm.ts
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 组件中渲染购物车商品:

  • Preact
  • React
  • Solid
  • Svelte
  • Vue
  • Lit
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;
}
src/components/CartFlyout.jsx
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;
}
src/components/CartFlyout.jsx
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;
}
src/components/CartFlyout.svelte
<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}
src/components/CartFlyout.vue
<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>
src/components/CartFlyout.ts
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);

现在,你应该拥有了一个完全交互式的电商示例,并且是宇宙中最小的 JS 包

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文