人人都会AI编程

28.2 电商前台:商品列表、购物车、下单流程

更新时间:2026-07-10

电商前台是前端开发中最经典、最综合的业务场景之一。它不仅涉及数据展示、状态同步、用户交互,还要处理复杂的跨页面数据流转和一致性问题。本节将以一个简化的电商应用为例,围绕 商品列表购物车下单流程 三个核心模块,讲解如何使用 React 生态进行落地实现。


整体架构与状态设计

在开始编码之前,先明确应用的基本页面结构和数据流:

页面路由:

  • / 商品列表页
  • /cart 购物车页
  • /checkout 下单结算页
  • /orders 订单列表页(可选)

核心状态:

  • 商品数据:从服务端获取,组件内部状态或通过 React Query 管理。
  • 购物车数据:全局共享,多个页面需要读写,适合用全局状态库(这里选用 Zustand)。
  • 用户信息:从登录态获取,可通过 Context 或状态库管理。

技术栈选型:

  • 路由:React Router v6
  • 数据请求:TanStack Query (React Query) + Axios
  • 全局状态:Zustand(购物车)
  • 样式:Tailwind CSS(为简洁,示例中省略具体样式类名)

一、商品列表

商品列表是入口页面,需支持商品展示、分类筛选、搜索、分页等功能。这里重点展示列表渲染、加载态处理和加入购物车交互。

1.1 定义商品类型

interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  stock: number;
}

1.2 封装数据请求 Hook

使用 React Query 获取商品数据,自动管理加载状态和缓存。

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchProducts = async (): Promise<Product[]> => {
  const { data } = await axios.get('/api/products');
  return data;
};

export const useProducts = () => {
  return useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
    staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜
  });
};

1.3 商品列表组件

在组件中使用 Hook,区分加载、错误、空数据等状态,最后渲染商品卡片。

import { useProducts } from './useProducts';
import { useCartStore } from './cartStore'; // 后面实现

const ProductList = () => {
  const { data: products, isLoading, error } = useProducts();
  const addItem = useCartStore(state => state.addItem);

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>加载失败</div>;
  if (!products?.length) return <div>暂无商品</div>;

  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={() => addItem({ ...product, quantity: 1 })}
        />
      ))}
    </div>
  );
};

1.4 商品卡片组件

卡片展示信息,并提供“加入购物车”按钮,按钮需考虑库存禁用。

const ProductCard = ({ product, onAddToCart }) => {
  const isSoldOut = product.stock <= 0;

  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>¥{product.price}</p>
      <button
        onClick={onAddToCart}
        disabled={isSoldOut}
      >
        {isSoldOut ? '已售罄' : '加入购物车'}
      </button>
    </div>
  );
};

加入购物车直接调用 addItem,全局状态实时更新,无需额外的上下文传递。


二、购物车

购物车需要支持增加/减少商品数量、删除商品、计算总价,并且数据在多个页面间保持一致。使用 Zustand 实现轻量且高性能的全局购物车 Store。

2.1 购物车状态管理(Zustand Store)

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  image: string;
  quantity: number;
  stock: number;
}

interface CartState {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
}

export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      addItem: (item) => {
        const existing = get().items.find(i => i.id === item.id);
        if (existing) {
          // 已存在则增加数量,但不超过库存
          set({
            items: get().items.map(i =>
              i.id === item.id
                ? { ...i, quantity: Math.min(i.quantity + 1, i.stock) }
                : i
            ),
          });
        } else {
          set({ items: [...get().items, { ...item, quantity: 1 }] });
        }
      },
      removeItem: (id) => {
        set({ items: get().items.filter(i => i.id !== id) });
      },
      updateQuantity: (id, quantity) => {
        if (quantity < 1) return;
        set({
          items: get().items.map(i =>
            i.id === id
              ? { ...i, quantity: Math.min(quantity, i.stock) }
              : i
          ),
        });
      },
      clearCart: () => set({ items: [] }),
      totalPrice: () => {
        return get().items.reduce((sum, i) => sum + i.price * i.quantity, 0);
      },
    }),
    {
      name: 'cart-storage', // 本地存储 key,用于持久化
    }
  )
);
  • addItem 处理重复商品数量叠加(考虑库存上限)。
  • updateQuantity 直接设置数量,同样受库存限制。
  • 使用 persist 中间件将购物车数据自动同步到 localStorage,刷新页面不丢失。

2.2 购物车页面组件

购物车页面读取全局状态,渲染商品列表、操作按钮和总价区域。

import { useCartStore } from './cartStore';
import { Link } from 'react-router-dom';

const CartPage = () => {
  const { items, removeItem, updateQuantity, totalPrice } = useCartStore();

  if (items.length === 0) {
    return <div>购物车是空的,<Link to="/">去逛逛</Link></div>;
  }

  return (
    <div className="cart-page">
      <h2>购物车</h2>
      {items.map(item => (
        <div key={item.id} className="cart-item">
          <img src={item.image} alt={item.name} />
          <div>
            <h4>{item.name}</h4>
            <p>¥{item.price}</p>
            <div>
              <button
                onClick={() => updateQuantity(item.id, item.quantity - 1)}
                disabled={item.quantity <= 1}
              >
                -
              </button>
              <span>{item.quantity}</span>
              <button
                onClick={() => updateQuantity(item.id, item.quantity + 1)}
                disabled={item.quantity >= item.stock}
              >
                +
              </button>
            </div>
          </div>
          <div>
            <p>小计:¥{item.price * item.quantity}</p>
            <button onClick={() => removeItem(item.id)}>删除</button>
          </div>
        </div>
      ))}
      <div className="cart-summary">
        <p>合计:¥{totalPrice()}</p>
        <Link to="/checkout">去结算</Link>
      </div>
    </div>
  );
};
  • 空购物车时引导用户返回商品列表。
  • 增减按钮受最小数量(1)和库存限制。
  • 总价直接调用 Store 中的方法实时计算。

三、下单流程

下单流程一般涉及:提交订单 → 校验库存 → 生成订单 → 清空购物车 → 跳转到支付或订单成功页。这里是前后端交互的典型场景。

3.1 下单页面与表单

结算页面需要收集收货地址、支付方式等信息,并展示最终要提交的订单摘要。

const CheckoutPage = () => {
  const { items, totalPrice, clearCart } = useCartStore();
  const navigate = useNavigate();

  const [address, setAddress] = useState('');
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async () => {
    if (items.length === 0) return;
    setSubmitting(true);
    try {
      await axios.post('/api/orders', {
        address,
        items: items.map(i => ({ productId: i.id, quantity: i.quantity })),
      });
      clearCart(); // 下单成功清空购物车
      navigate('/orders/success', { state: { message: '下单成功' } });
    } catch (error) {
      alert('下单失败,请重试');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="checkout-page">
      <h2>订单确认</h2>
      <div>
        <h3>收货地址</h3>
        <textarea
          value={address}
          onChange={e => setAddress(e.target.value)}
          placeholder="请输入详细地址"
        />
      </div>
      <div>
        <h3>商品列表</h3>
        {items.map(item => (
          <p key={item.id}>{item.name} × {item.quantity} —— ¥{item.price * item.quantity}</p>
        ))}
      </div>
      <p>应付总额:¥{totalPrice()}</p>
      <button onClick={handleSubmit} disabled={submitting || !address}>
        {submitting ? '提交中...' : '确认下单'}
      </button>
    </div>
  );
};

3.2 下单接口设计要点

  • 后端需做最终库存校验,防止前端绕过限制。
  • 返回订单号,前端可据此展示结果页。
  • 失败时返回具体错误码(如库存不足),前端给出友好提示。

3.3 处理下单过程中的异常

在实际项目中,下单可能存在并发、网络中断等问题:

  • 防抖:提交按钮在请求期间禁用,防止重复提交。
  • 乐观更新:如果希望体验更好,可以先在前端扣减库存显示,但最终以后端校验为准。
  • 回滚:若订单创建失败,不做任何本地状态修改。

3.4 下单后流程

下单成功后一般跳转到成功页,可展示订单号、预计配送等信息。

const OrderSuccessPage = () => {
  const location = useLocation();
  const message = location.state?.message ?? '下单成功';

  return <div>{message},感谢您的购买!</div>;
};

同时也可以提供查看订单详情的链接。


完整全链路数据流回顾

  1. 商品列表通过 React Query 异步获取,缓存优化性能。
  2. 加入购物车触发 Zustand Store 的 addItem,数据立即同步到全局状态,并由 persist 中间件自动持久化到本地存储。
  3. 购物车页面读取 Store 中的 items,渲染、修改数量、删除,变化实时反映。
  4. 下单页面读取购物车数据展示订单摘要,收集收货信息,调用下单接口;成功后清空购物车并跳转。
  5. 所有组件均通过响应式状态驱动,无需手动同步,数据流向清晰可预测。

这种架构简洁且可扩展,适用于从个人项目到中大型电商的开发。通过合理拆分职责(展示、状态管理、异步请求),你能在高内聚低耦合的前提下快速迭代功能。