Skip to main content

前端开发实战

今天用Claude Code从零开始构建一个电商产品展示平台。这个项目会涵盖前端开发的核心要素:响应式UI、状态管理、组件库、性能优化。

项目概述

要做什么

做一个电商产品展示平台,包括:

  • 响应式UI,适配各种设备
  • 全局状态管理(Context + Hooks)
  • 可复用组件库
  • 性能优化(代码分割、懒加载)
  • 动画和交互体验
  • 打包优化

技术栈

  • 框架: React 18 + React Router v6
  • 状态管理: React Context API + 自定义Hooks
  • 样式方案: CSS Modules + Styled Components
  • 构建工具: Vite(替代Create React App)
  • UI组件: 自建组件库 + Headless UI
  • 动画: Framer Motion
  • 表单: React Hook Form + Zod验证
  • 工具: Claude Code作为主要开发工具

需要多久

  • 传统开发: 3-5天
  • 用Claude Code: 3-5小时
  • 效率提升: 8-10倍

准备工作

环境要求

# 检查Node.js版本(需要18+)
node --version

# 检查npm版本
npm --version

# 检查Git版本
git --version

创建项目

# 使用Vite创建React项目
npm create vite@latest ecommerce-frontend -- --template react

# 进入项目目录
cd ecommerce-frontend

# 初始化Git仓库
git init
git branch -M main

# 安装依赖
npm install

启动Claude Code

# 在项目目录启动Claude Code
claude

第一阶段:项目架构设计

第一轮对话

我要创建一个电商产品展示平台:

1. 产品列表页(过滤、搜索、排序)
2. 产品详情页
3. 购物车
4. 收藏功能
5. 响应式设计
6. 性能优化

技术栈:
- React 18 + Vite
- React Router v6
- 自定义状态管理(Context + Hooks)
- CSS Modules + Tailwind CSS

帮我设计项目结构并创建基础文件。

Claude Code会创建完整的项目结构:

📁 创建项目结构...

✓ 创建 src/components/common/
✓ 创建 src/components/pages/
✓ 创建 src/context/
✓ 创建 src/hooks/
✓ 创建 src/services/
✓ 创建 src/utils/
✓ 创建 src/styles/
✓ 创建 src/assets/
✓ 更新路由配置

项目结构

ecommerce-frontend/
├── src/
│ ├── components/
│ │ ├── common/
│ │ │ ├── Button/
│ │ │ ├── Card/
│ │ │ ├── Modal/
│ │ │ ├── Input/
│ │ │ ├── Select/
│ │ │ └── LoadingSpinner/
│ │ ├── layout/
│ │ │ ├── Header/
│ │ │ ├── Footer/
│ │ │ ├── Sidebar/
│ │ │ └── Navigation/
│ │ └── features/
│ │ ├── ProductCard/
│ │ ├── ProductList/
│ │ ├── ProductFilter/
│ │ ├── ShoppingCart/
│ │ └── Wishlist/
│ ├── pages/
│ │ ├── Home/
│ │ ├── Products/
│ │ ├── ProductDetail/
│ │ ├── Cart/
│ │ ├── Wishlist/
│ │ └── NotFound/
│ ├── context/
│ │ ├── AuthContext.js
│ │ ├── CartContext.js
│ │ ├── WishlistContext.js
│ │ └── ThemeContext.js
│ ├── hooks/
│ │ ├── useCart.js
│ │ ├── useWishlist.js
│ │ ├── useLocalStorage.js
│ │ ├── useDebounce.js
│ │ └── useIntersectionObserver.js
│ ├── services/
│ │ ├── api.js
│ │ ├── productService.js
│ │ └── analytics.js
│ ├── utils/
│ │ ├── format.js
│ │ ├── validation.js
│ │ └── constants.js
│ ├── styles/
│ │ ├── global.css
│ │ ├── variables.css
│ │ └── animations.css
│ ├── App.jsx
│ └── main.jsx
├── public/
│ └── favicon.svg
├── index.html
├── vite.config.js
├── tailwind.config.js
└── package.json

第二阶段:UI组件开发

创建基础组件库

先创建可复用的基础组件:
1. Button组件(多种样式和尺寸)
2. Card组件
3. Input组件
4. Modal组件
5. LoadingSpinner组件

每个组件都要支持主题切换。

Button组件

src/components/common/Button/Button.jsx:

import React from 'react';
import styles from './Button.module.css';

const Button = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
fullWidth = false,
icon,
onClick,
type = 'button',
className = '',
...props
}) => {
const buttonClasses = [
styles.button,
styles[variant],
styles[size],
fullWidth && styles.fullWidth,
disabled && styles.disabled,
loading && styles.loading,
className,
]
.filter(Boolean)
.join(' ');

return (
<button
type={type}
className={buttonClasses}
onClick={onClick}
disabled={disabled || loading}
{...props}
>
{loading && <span className={styles.spinner} />}
{icon && !loading && <span className={styles.icon}>{icon}</span>}
<span className={styles.content}>{children}</span>
</button>
);
};

export default Button;

src/components/common/Button/Button.module.css:

.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: none;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}

.button:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}

/* Variants */
.primary {
background: var(--primary-color);
color: white;
}

.primary:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}

.secondary {
background: white;
color: var(--text-primary);
border: 2px solid var(--border-color);
}

.secondary:hover:not(:disabled) {
border-color: var(--primary-color);
color: var(--primary-color);
}

.outline {
background: transparent;
color: var(--primary-color);
border: 2px solid var(--primary-color);
}

.outline:hover:not(:disabled) {
background: var(--primary-color);
color: white;
}

.ghost {
background: transparent;
color: var(--text-secondary);
}

.ghost:hover:not(:disabled) {
background: var(--bg-gray);
color: var(--text-primary);
}

.danger {
background: var(--danger-color);
color: white;
}

.danger:hover:not(:disabled) {
background: #dc2626;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}

/* Sizes */
.small {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}

.medium {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}

.large {
padding: 1rem 2rem;
font-size: 1.125rem;
}

/* States */
.disabled {
opacity: 0.5;
cursor: not-allowed;
}

.loading {
pointer-events: none;
}

.fullWidth {
width: 100%;
}

.spinner {
width: 1rem;
height: 1rem;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}

.icon {
display: inline-flex;
align-items: center;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}

/* 响应式设计 */
@media (max-width: 768px) {
.medium {
padding: 0.625rem 1.25rem;
font-size: 0.9375rem;
}

.large {
padding: 0.875rem 1.75rem;
font-size: 1rem;
}
}

Card组件

src/components/common/Card/Card.jsx:

import React from 'react';
import styles from './Card.module.css';

const Card = ({
children,
variant = 'default',
hoverable = false,
padding = 'medium',
shadow = 'medium',
className = '',
onClick,
...props
}) => {
const cardClasses = [
styles.card,
styles[variant],
styles[padding],
styles[shadow],
hoverable && styles.hoverable,
onClick && styles.clickable,
className,
]
.filter(Boolean)
.join(' ');

return (
<div
className={cardClasses}
onClick={onClick}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
{...props}
>
{children}
</div>
);
};

export default Card;

src/components/common/Card/Card.module.css:

.card {
background: white;
border-radius: 1rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}

/* Variants */
.default {
border: 1px solid var(--border-color);
}

.bordered {
border: 2px solid var(--border-color);
}

.elevated {
border: none;
}

/* Padding */
.small {
padding: 1rem;
}

.medium {
padding: 1.5rem;
}

.large {
padding: 2rem;
}

/* Shadow */
.none {
box-shadow: none;
}

.small {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.medium {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

.large {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

/* Hover effect */
.hoverable:hover {
transform: translateY(-4px);
}

.hoverable.medium:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}

.clickable {
cursor: pointer;
}

.clickable:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}

/* 响应式 */
@media (max-width: 768px) {
.medium {
padding: 1rem;
}

.large {
padding: 1.5rem;
}
}

Modal组件

src/components/common/Modal/Modal.jsx:

import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import styles from './Modal.module.css';

const Modal = ({
isOpen = false,
onClose,
title,
children,
size = 'medium',
closeOnOverlayClick = true,
closeOnEscape = true,
showCloseButton = true,
className = '',
}) => {
const modalRef = useRef(null);
const previousActiveElement = useRef(null);

useEffect(() => {
if (isOpen) {
// 保存当前聚焦元素
previousActiveElement.current = document.activeElement;

// 禁用背景滚动
document.body.style.overflow = 'hidden';

// 聚焦到modal
modalRef.current?.focus();

// 监听ESC键
if (closeOnEscape) {
const handleEscape = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
} else {
// 恢复背景滚动
document.body.style.overflow = '';

// 恢复焦点
previousActiveElement.current?.focus();
}
}, [isOpen, closeOnEscape, onClose]);

if (!isOpen) return null;

const handleOverlayClick = (e) => {
if (closeOnOverlayClick && e.target === e.currentTarget) {
onClose();
}
};

const modalContent = (
<div
className={styles.overlay}
onClick={handleOverlayClick}
aria-modal="true"
role="dialog"
aria-labelledby={title ? 'modal-title' : undefined}
>
<div
ref={modalRef}
className={`${styles.modal} ${styles[size]} ${className}`}
tabIndex={-1}
>
{(title || showCloseButton) && (
<div className={styles.header}>
{title && <h2 id="modal-title" className={styles.title}>{title}</h2>}
{showCloseButton && (
<button
onClick={onClose}
className={styles.closeButton}
aria-label="关闭"
>

</button>
)}
</div>
)}
<div className={styles.content}>
{children}
</div>
</div>
</div>
);

return createPortal(modalContent, document.body);
};

export default Modal;

src/components/common/Modal/Modal.module.css:

.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease;
}

.modal {
background: white;
border-radius: 1rem;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

/* Sizes */
.small {
width: 100%;
max-width: 400px;
}

.medium {
width: 100%;
max-width: 600px;
}

.large {
width: 100%;
max-width: 800px;
}

.fullscreen {
width: 100%;
height: 100%;
max-width: none;
max-height: 100%;
border-radius: 0;
}

.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
}

.title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}

.closeButton {
background: transparent;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
line-height: 1;
border-radius: 0.375rem;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}

.closeButton:hover {
background: var(--bg-gray);
color: var(--text-primary);
}

.content {
padding: 1.5rem;
overflow-y: auto;
}

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

/* 响应式 */
@media (max-width: 768px) {
.overlay {
padding: 0.5rem;
}

.header,
.content {
padding: 1rem;
}

.small,
.medium,
.large {
max-width: 100%;
}
}

Input组件

src/components/common/Input/Input.jsx:

import React, { forwardRef } from 'react';
import styles from './Input.module.css';

const Input = forwardRef((
{
label,
error,
icon,
helperText,
fullWidth = false,
variant = 'outlined',
size = 'medium',
className = '',
...props
},
ref
) => {
const inputClasses = [
styles.input,
styles[variant],
styles[size],
error && styles.error,
icon && styles.withIcon,
fullWidth && styles.fullWidth,
className,
]
.filter(Boolean)
.join(' ');

const wrapperClasses = [
styles.wrapper,
fullWidth && styles.fullWidth,
]
.filter(Boolean)
.join(' ');

return (
<div className={wrapperClasses}>
{label && <label className={styles.label}>{label}</label>}
<div className={styles.inputContainer}>
{icon && <span className={styles.icon}>{icon}</span>}
<input
ref={ref}
className={inputClasses}
{...props}
/>
</div>
{error && <span className={styles.errorText}>{error}</span>}
{helperText && !error && (
<span className={styles.helperText}>{helperText}</span>
)}
</div>
);
});

Input.displayName = 'Input';

export default Input;

src/components/common/Input/Input.module.css:

.wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.fullWidth {
width: 100%;
}

.label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}

.inputContainer {
position: relative;
display: flex;
align-items: center;
}

.input {
width: 100%;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
font-size: 1rem;
color: var(--text-primary);
background: white;
transition: all 0.2s ease;
}

.input::placeholder {
color: var(--text-placeholder);
}

.input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}

/* Variants */
.outlined {
background: white;
}

.filled {
background: var(--bg-gray);
border-color: transparent;
}

.filled:focus {
background: white;
border-color: var(--primary-color);
}

.underlined {
border: none;
border-bottom: 2px solid var(--border-color);
border-radius: 0;
padding-left: 0;
padding-right: 0;
}

.underlined:focus {
border-color: var(--primary-color);
box-shadow: none;
}

/* Sizes */
.small {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}

.medium {
padding: 0.75rem 1rem;
font-size: 1rem;
}

.large {
padding: 1rem 1.25rem;
font-size: 1.125rem;
}

/* States */
.error {
border-color: var(--danger-color);
}

.error:focus {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}

.withIcon {
padding-left: 2.5rem;
}

.icon {
position: absolute;
left: 0.75rem;
color: var(--text-secondary);
pointer-events: none;
}

.errorText {
font-size: 0.75rem;
color: var(--danger-color);
}

.helperText {
font-size: 0.75rem;
color: var(--text-secondary);
}

/* 响应式 */
@media (max-width: 768px) {
.medium {
padding: 0.625rem 0.875rem;
}

.large {
padding: 0.875rem 1.125rem;
font-size: 1rem;
}
}

第三阶段:全局状态管理

创建Context系统

你: 创建一个基于React Context的状态管理系统,包括:
1. 购物车状态(CartContext)
2. 收藏夹状态(WishlistContext)
3. 认证状态(AuthContext)
4. 主题状态(ThemeContext)

每个Context都要提供对应的自定义Hook,并实现持久化存储。

CartContext

src/context/CartContext.jsx:

import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { loadFromLocalStorage, saveToLocalStorage } from '../utils/storage';

const CartContext = createContext();

const CART_STORAGE_KEY = 'shopping_cart';

// 初始状态
const initialState = {
items: [],
totalItems: 0,
totalPrice: 0,
isLoading: false,
error: null,
};

// Reducer
function cartReducer(state, action) {
switch (action.type) {
case 'CART_LOAD_START':
return {
...state,
isLoading: true,
error: null,
};

case 'CART_LOAD_SUCCESS':
return {
...state,
isLoading: false,
...action.payload,
};

case 'CART_LOAD_ERROR':
return {
...state,
isLoading: false,
error: action.payload,
};

case 'CART_ADD_ITEM': {
const existingItemIndex = state.items.findIndex(
(item) => item.id === action.payload.id
);

let newItems;

if (existingItemIndex > -1) {
// Item already exists, update quantity
newItems = state.items.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + action.payload.quantity }
: item
);
} else {
// Add new item
newItems = [...state.items, { ...action.payload }];
}

const totals = calculateTotals(newItems);

return {
...state,
items: newItems,
...totals,
};
}

case 'CART_REMOVE_ITEM': {
const newItems = state.items.filter((item) => item.id !== action.payload);
const totals = calculateTotals(newItems);

return {
...state,
items: newItems,
...totals,
};
}

case 'CART_UPDATE_QUANTITY': {
const newItems = state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);

const totals = calculateTotals(newItems);

return {
...state,
items: newItems,
...totals,
};
}

case 'CART_CLEAR':
return {
...state,
items: [],
totalItems: 0,
totalPrice: 0,
};

case 'CART_SET_ITEMS': {
const totals = calculateTotals(action.payload);
return {
...state,
items: action.payload,
...totals,
};
};

default:
return state;
}
}

// 计算总数和总价
function calculateTotals(items) {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);

return {
totalItems,
totalPrice: parseFloat(totalPrice.toFixed(2)),
};
}

// Provider组件
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState);

// 从本地存储加载购物车
useEffect(() => {
const loadCart = () => {
dispatch({ type: 'CART_LOAD_START' });

try {
const savedCart = loadFromLocalStorage(CART_STORAGE_KEY);
if (savedCart) {
dispatch({
type: 'CART_LOAD_SUCCESS',
payload: savedCart,
});
} else {
dispatch({ type: 'CART_LOAD_SUCCESS', payload: initialState });
}
} catch (error) {
dispatch({
type: 'CART_LOAD_ERROR',
payload: '加载购物车失败',
});
}
};

loadCart();
}, []);

// 保存购物车到本地存储
useEffect(() => {
if (!state.isLoading) {
saveToLocalStorage(CART_STORAGE_KEY, {
items: state.items,
totalItems: state.totalItems,
totalPrice: state.totalPrice,
});
}
}, [state.items, state.isLoading]);

// Actions
const addToCart = (product, quantity = 1) => {
dispatch({
type: 'CART_ADD_ITEM',
payload: {
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity,
addedAt: new Date().toISOString(),
},
});
};

const removeFromCart = (productId) => {
dispatch({
type: 'CART_REMOVE_ITEM',
payload: productId,
});
};

const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
} else {
dispatch({
type: 'CART_UPDATE_QUANTITY',
payload: { id: productId, quantity },
});
}
};

const clearCart = () => {
dispatch({ type: 'CART_CLEAR' });
};

const isInCart = (productId) => {
return state.items.some((item) => item.id === productId);
};

const getItemQuantity = (productId) => {
const item = state.items.find((item) => item.id === productId);
return item ? item.quantity : 0;
};

const value = {
...state,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
isInCart,
getItemQuantity,
};

return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};

// 自定义Hook
export const useCart = () => {
const context = useContext(CartContext);

if (!context) {
throw new Error('useCart must be used within CartProvider');
}

return context;
};

WishlistContext

src/context/WishlistContext.jsx:

import React, { createContext, useContext, useState, useEffect } from 'react';
import { loadFromLocalStorage, saveToLocalStorage } from '../utils/storage';

const WishlistContext = createContext();

const WISHLIST_STORAGE_KEY = 'user_wishlist';

export const WishlistProvider = ({ children }) => {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);

// 加载收藏夹
useEffect(() => {
const loadWishlist = () => {
try {
const savedWishlist = loadFromLocalStorage(WISHLIST_STORAGE_KEY);
setItems(savedWishlist || []);
} catch (error) {
console.error('加载收藏夹失败:', error);
} finally {
setIsLoading(false);
}
};

loadWishlist();
}, []);

// 保存收藏夹
useEffect(() => {
if (!isLoading) {
saveToLocalStorage(WISHLIST_STORAGE_KEY, items);
}
}, [items, isLoading]);

// 添加到收藏夹
const addToWishlist = (product) => {
setItems((prevItems) => {
const exists = prevItems.some((item) => item.id === product.id);
if (exists) {
return prevItems;
}
return [
...prevItems,
{
...product,
addedAt: new Date().toISOString(),
},
];
});
};

// 从收藏夹移除
const removeFromWishlist = (productId) => {
setItems((prevItems) => prevItems.filter((item) => item.id !== productId));
};

// 切换收藏状态
const toggleWishlist = (product) => {
const exists = items.some((item) => item.id === product.id);
if (exists) {
removeFromWishlist(product.id);
} else {
addToWishlist(product);
}
};

// 检查是否已收藏
const isInWishlist = (productId) => {
return items.some((item) => item.id === productId);
};

// 清空收藏夹
const clearWishlist = () => {
setItems([]);
};

const value = {
items,
isLoading,
addToWishlist,
removeFromWishlist,
toggleWishlist,
isInWishlist,
clearWishlist,
itemCount: items.length,
};

return (
<WishlistContext.Provider value={value}>
{children}
</WishlistContext.Provider>
);
};

// 自定义Hook
export const useWishlist = () => {
const context = useContext(WishlistContext);

if (!context) {
throw new Error('useWishlist must be used within WishlistProvider');
}

return context;
};

ThemeContext

src/context/ThemeContext.jsx:

import React, { createContext, useContext, useState, useEffect } from 'react';
import { loadFromLocalStorage, saveToLocalStorage } from '../utils/storage';

const ThemeContext = createContext();

const THEME_STORAGE_KEY = 'app_theme';

export const themes = {
light: 'light',
dark: 'dark',
system: 'system',
};

export const ThemeProvider = ({ children }) => {
const [theme, setThemeState] = useState(themes.system);
const [resolvedTheme, setResolvedTheme] = useState('light');

// 初始化主题
useEffect(() => {
const savedTheme = loadFromLocalStorage(THEME_STORAGE_KEY);
if (savedTheme && Object.values(themes).includes(savedTheme)) {
setThemeState(savedTheme);
}
}, []);

// 应用主题
useEffect(() => {
const applyTheme = () => {
let themeToApply = theme;

if (theme === themes.system) {
// 检测系统主题偏好
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
themeToApply = prefersDark ? themes.dark : themes.light;
}

setResolvedTheme(themeToApply);

// 更新DOM
document.documentElement.setAttribute('data-theme', themeToApply);

// 更新meta theme-color
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute(
'content',
themeToApply === themes.dark ? '#1a1a1a' : '#ffffff'
);
}
};

applyTheme();

// 监听系统主题变化
if (theme === themes.system) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => applyTheme();
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}
}, [theme]);

// 切换主题
const setTheme = (newTheme) => {
if (Object.values(themes).includes(newTheme)) {
setThemeState(newTheme);
saveToLocalStorage(THEME_STORAGE_KEY, newTheme);
}
};

// 切换到下一个主题
const toggleTheme = () => {
const themeOrder = [themes.light, themes.dark, themes.system];
const currentIndex = themeOrder.indexOf(theme);
const nextIndex = (currentIndex + 1) % themeOrder.length;
setTheme(themeOrder[nextIndex]);
};

const value = {
theme,
resolvedTheme,
setTheme,
toggleTheme,
isDark: resolvedTheme === themes.dark,
};

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};

// 自定义Hook
export const useTheme = () => {
const context = useContext(ThemeContext);

if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}

return context;
};

第四阶段:自定义Hooks开发

你: 创建实用的自定义Hooks:
1. useLocalStorage - 本地存储同步
2. useDebounce - 防抖处理
3. useIntersectionObserver - 懒加载和无限滚动
4. usePrevious - 获取上一次的值
5. useMediaQuery - 响应式检测
6. useAsync - 异步状态管理

useLocalStorage

src/hooks/useLocalStorage.js:

import { useState, useEffect, useCallback } from 'react';

export function useLocalStorage(key, initialValue) {
// 获取初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error loading ${key} from localStorage:`, error);
return initialValue;
}
});

// 更新本地存储和状态
const setValue = useCallback(
(value) => {
try {
// 支持函数更新
const valueToStore =
value instanceof Function ? value(storedValue) : value;

setStoredValue(valueToStore);

// 保存到本地存储
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error saving ${key} to localStorage:`, error);
}
},
[key, storedValue]
);

// 从其他标签页同步变化
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key && e.newValue !== null) {
try {
setStoredValue(JSON.parse(e.newValue));
} catch (error) {
console.error('Error parsing storage change:', error);
}
}
};

window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);

return [storedValue, setValue];
}

useDebounce

src/hooks/useDebounce.js:

import { useState, useEffect } from 'react';

export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
// 设置定时器
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

// 清除定时器
return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
}

// 使用示例:
// const [searchTerm, setSearchTerm] = useState('');
// const debouncedSearchTerm = useDebounce(searchTerm, 500);
//
// useEffect(() => {
// // 使用防抖后的搜索词进行API调用
// if (debouncedSearchTerm) {
// searchProducts(debouncedSearchTerm);
// }
// }, [debouncedSearchTerm]);

useIntersectionObserver

src/hooks/useIntersectionObserver.js:

import { useState, useEffect, useRef } from 'react';

export function useIntersectionObserver(options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const [hasIntersected, setHasIntersected] = useState(false);
const targetRef = useRef(null);

useEffect(() => {
const target = targetRef.current;
if (!target) return;

const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);

if (entry.isIntersecting && !hasIntersected) {
setHasIntersected(true);
}
},
{
threshold: 0.1,
rootMargin: '0px',
...options,
}
);

observer.observe(target);

return () => {
observer.unobserve(target);
};
}, [options.threshold, options.rootMargin, hasIntersected]);

return [targetRef, isIntersecting, hasIntersected];
}

// 使用示例 - 懒加载图片:
// const [imgRef, isVisible] = useIntersectionObserver();
//
// <img
// ref={imgRef}
// src={isVisible ? imageUrl : placeholderUrl}
// alt="Product"
// />

// 使用示例 - 无限滚动:
// const [loadMoreRef, isVisible] = useIntersectionObserver();
//
// useEffect(() => {
// if (isVisible) {
// loadMoreProducts();
// }
// }, [isVisible]);

useMediaQuery

src/hooks/useMediaQuery.js:

import { useState, useEffect } from 'react';

export function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window !== 'undefined') {
return window.matchMedia(query).matches;
}
return false;
});

useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);

// 现代浏览器
mediaQuery.addEventListener('change', handler);

return () => {
mediaQuery.removeEventListener('change', handler);
};
}, [query]);

return matches;
}

// 使用示例:
// const isMobile = useMediaQuery('(max-width: 768px)');
// const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
// const isPrint = useMediaQuery('print');

useAsync

src/hooks/useAsync.js:

import { useState, useEffect, useCallback } from 'react';

export function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState('idle');
const [data, setData] = useState(null);
const [error, setError] = useState(null);

// 执行异步函数
const execute = useCallback(
async (...args) => {
setStatus('pending');
setData(null);
setError(null);

try {
const response = await asyncFunction(...args);
setData(response);
setStatus('success');
return response;
} catch (error) {
setError(error);
setStatus('error');
throw error;
}
},
[asyncFunction]
);

// 立即执行
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);

return {
execute,
status,
data,
error,
isLoading: status === 'pending',
isError: status === 'error',
isSuccess: status === 'success',
isIdle: status === 'idle',
};
}

// 使用示例:
// const { data, isLoading, error, execute } = useAsync(fetchProducts);
//
// if (isLoading) return <LoadingSpinner />;
// if (error) return <Error message={error.message} />;
// return <ProductList products={data} />;

第五阶段:功能组件开发

ProductCard组件

src/components/features/ProductCard/ProductCard.jsx:

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import Card from '../../common/Card/Card';
import Button from '../../common/Button/Button';
import { useCart } from '../../../context/CartContext';
import { useWishlist } from '../../../context/WishlistContext';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import styles from './ProductCard.module.css';

const ProductCard = ({ product }) => {
const { addToCart, isInCart } = useCart();
const { toggleWishlist, isInWishlist } = useWishlist();
const [imageLoaded, setImageLoaded] = useState(false);

const [imgRef, isVisible] = useIntersectionObserver({
threshold: 0.01,
triggerOnce: true,
});

const inCart = isInCart(product.id);
const inWishlist = isInWishlist(product.id);

const handleAddToCart = (e) => {
e.preventDefault();
e.stopPropagation();
addToCart(product);
};

const handleToggleWishlist = (e) => {
e.preventDefault();
e.stopPropagation();
toggleWishlist(product);
};

const discount = product.discount || 0;
const originalPrice = product.price;
const discountedPrice = discount > 0
? (originalPrice * (1 - discount / 100)).toFixed(2)
: originalPrice;

return (
<Link to={`/products/${product.id}`} className={styles.link}>
<Card
hoverable
className={styles.card}
>
<div ref={imgRef} className={styles.imageContainer}>
{isVisible && (
<img
src={product.image}
alt={product.name}
className={`${styles.image} ${imageLoaded ? styles.loaded : ''}`}
onLoad={() => setImageLoaded(true)}
loading="lazy"
/>
)}
{!imageLoaded && <div className={styles.imagePlaceholder} />}

{discount > 0 && (
<span className={styles.discountBadge}>
-{discount}%
</span>
)}

<button
className={`${styles.wishlistButton} ${inWishlist ? styles.active : ''}`}
onClick={handleToggleWishlist}
aria-label={inWishlist ? '从收藏移除' : '添加到收藏'}
>
{inWishlist ? '❤️' : '🤍'}
</button>
</div>

<div className={styles.content}>
<div className={styles.category}>{product.category}</div>
<h3 className={styles.name}>{product.name}</h3>

<div className={styles.rating}>
<span className={styles.stars}>
{'★'.repeat(Math.floor(product.rating))}
{'☆'.repeat(5 - Math.floor(product.rating))}
</span>
<span className={styles.reviews}>({product.reviews})</span>
</div>

<div className={styles.priceContainer}>
<div className={styles.prices}>
<span className={styles.currentPrice}>¥{discountedPrice}</span>
{discount > 0 && (
<span className={styles.originalPrice}>¥{originalPrice}</span>
)}
</div>

<Button
size="small"
variant={inCart ? 'outline' : 'primary'}
onClick={handleAddToCart}
className={styles.cartButton}
>
{inCart ? '✓ 已加购' : '+ 加入'}
</Button>
</div>
</div>
</Card>
</Link>
);
};

export default ProductCard;

src/components/features/ProductCard/ProductCard.module.css:

.link {
text-decoration: none;
color: inherit;
display: block;
}

.card {
height: 100%;
display: flex;
flex-direction: column;
}

.imageContainer {
position: relative;
padding-top: 100%; /* 1:1 Aspect Ratio */
overflow: hidden;
background: var(--bg-gray);
}

.image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease;
}

.image.loaded {
opacity: 1;
}

.imagePlaceholder {
position: absolute;
inset: 0;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

.discountBadge {
position: absolute;
top: 0.75rem;
left: 0.75rem;
background: var(--danger-color);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
z-index: 1;
}

.wishlistButton {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: white;
border: none;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}

.wishlistButton:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.wishlistButton.active {
background: #fff5f5;
}

.content {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex: 1;
}

.category {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}

.name {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.75rem 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

.rating {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}

.stars {
color: var(--warning-color);
letter-spacing: 0.05em;
}

.reviews {
font-size: 0.875rem;
color: var(--text-secondary);
}

.priceContainer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}

.prices {
display: flex;
align-items: baseline;
gap: 0.5rem;
flex-wrap: wrap;
}

.currentPrice {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
}

.originalPrice {
font-size: 1rem;
color: var(--text-secondary);
text-decoration: line-through;
}

.cartButton {
flex-shrink: 0;
}

/* 响应式 */
@media (max-width: 768px) {
.content {
padding: 1rem;
}

.name {
font-size: 1rem;
}

.currentPrice {
font-size: 1.25rem;
}

.priceContainer {
flex-direction: column;
align-items: flex-start;
}

.cartButton {
width: 100%;
}
}

ProductList组件(带无限滚动)

src/components/features/ProductList/ProductList.jsx:

import React, { useState, useEffect, useCallback } from 'react';
import ProductCard from '../ProductCard/ProductCard';
import LoadingSpinner from '../../common/LoadingSpinner/LoadingSpinner';
import { useAsync, useIntersectionObserver } from '../../../hooks';
import styles from './ProductList.module.css';

const ProductList = ({ fetchProducts, filters = {}, pageSize = 12 }) => {
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [allFilters, setAllFilters] = useState(filters);

// 使用useAsync获取数据
const { data: newProducts, isLoading, error, execute } = useAsync(
() => fetchProducts({ ...allFilters, page, pageSize }),
false
);

// 更新过滤条件
useEffect(() => {
setAllFilters(filters);
setProducts([]);
setPage(1);
setHasMore(true);
}, [filters]);

// 加载数据
useEffect(() => {
if (page > 0) {
execute();
}
}, [page, allFilters, execute]);

// 处理新数据
useEffect(() => {
if (newProducts) {
if (page === 1) {
setProducts(newProducts);
} else {
setProducts((prev) => [...prev, ...newProducts]);
}

// 检查是否还有更多数据
if (newProducts.length < pageSize) {
setHasMore(false);
}
}
}, [newProducts, page, pageSize]);

// 无限滚动
const [loadMoreRef, isVisible] = useIntersectionObserver({
threshold: 0.1,
});

useEffect(() => {
if (isVisible && hasMore && !isLoading && page > 0) {
setPage((prev) => prev + 1);
}
}, [isVisible, hasMore, isLoading, page]);

if (error) {
return (
<div className={styles.error}>
<p>加载失败: {error.message}</p>
<button onClick={() => execute()} className={styles.retryButton}>
重试
</button>
</div>
);
}

return (
<div className={styles.container}>
<div className={styles.grid}>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>

{isLoading && products.length === 0 && (
<div className={styles.initialLoading}>
<LoadingSpinner size="large" />
</div>
)}

{isLoading && products.length > 0 && (
<div className={styles.paginationLoading}>
<LoadingSpinner size="medium" />
</div>
)}

{!isLoading && !hasMore && products.length > 0 && (
<div className={styles.endMessage}>
<p>没有更多商品了</p>
</div>
)}

{!isLoading && products.length === 0 && (
<div className={styles.empty}>
<p>暂无商品</p>
</div>
)}

{hasMore && !isLoading && (
<div ref={loadMoreRef} className={styles.loadMoreTrigger} />
)}
</div>
);
};

export default ProductList;

第六阶段:性能优化

图片懒加载和优化

你: 实现图片优化方案:
1. 懒加载(useIntersectionObserver)
2. 响应式图片(srcset)
3. WebP格式支持
4. 图片占位符
5. BlurHash生成模糊预览

src/components/common/OptimizedImage/OptimizedImage.jsx:

import React, { useState, useRef } from 'react';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import styles from './OptimizedImage.module.css';

const OptimizedImage = ({
src,
alt,
width,
height,
sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
blurHash,
className = '',
...props
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
const [imgRef, isVisible] = useIntersectionObserver({
threshold: 0.01,
triggerOnce: true,
});

const handleError = () => {
setHasError(true);
};

const handleLoad = () => {
setIsLoaded(true);
};

// 生成srcset
const generateSrcSet = (baseSrc) => {
const sizes = [400, 800, 1200, 1600];
return sizes
.map((size) => `${baseSrc}?w=${size} ${size}w`)
.join(', ');
};

return (
<div
ref={imgRef}
className={`${styles.container} ${className}`}
style={{ width, height }}
>
{blurHash && (
<div
className={styles.blurHash}
style={{ backgroundImage: `url(${blurHash})` }}
/>
)}

{hasError ? (
<div className={styles.error}>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
) : (
isVisible && (
<img
src={src}
alt={alt}
srcSet={generateSrcSet(src)}
sizes={sizes}
loading="lazy"
decoding="async"
onLoad={handleLoad}
onError={handleError}
className={`${styles.image} ${isLoaded ? styles.loaded : ''}`}
{...props}
/>
)
)}

{!isLoaded && !hasError && (
<div className={styles.skeleton} />
)}
</div>
);
};

export default OptimizedImage;

代码分割和懒加载

src/App.jsx:

import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/common/LoadingSpinner/LoadingSpinner';
import Layout from './components/layout/Layout/Layout';

// 路由级别代码分割
const Home = lazy(() => import('./pages/Home/Home'));
const Products = lazy(() => import('./pages/Products/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart/Cart'));
const Wishlist = lazy(() => import('./pages/Wishlist/Wishlist'));
const NotFound = lazy(() => import('./pages/NotFound/NotFound'));

// 加载中组件
const PageLoader = () => (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<LoadingSpinner size="large" />
</div>
);

function App() {
return (
<BrowserRouter>
<Layout>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/wishlist" element={<Wishlist />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Layout>
</BrowserRouter>
);
}

export default App;

虚拟化长列表

src/components/common/VirtualList/VirtualList.jsx:

import React, { useRef, useEffect, useState } from 'react';

const VirtualList = ({
items,
itemHeight,
containerHeight,
renderItem,
overscan = 3,
}) => {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);

const totalHeight = items.length * itemHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + containerHeight) / itemHeight) + overscan
);

const visibleItems = items.slice(startIndex, endIndex + 1);

const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};

return (
<div
ref={containerRef}
className="virtual-list-container"
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{
position: 'absolute',
top: (startIndex + index) * itemHeight,
height: itemHeight,
width: '100%',
}}
>
{renderItem(item, startIndex + index)}
</div>
))}
</div>
</div>
);
};

export default VirtualList;

// 使用示例:
// <VirtualList
// items={products}
// itemHeight={300}
// containerHeight={600}
// renderItem={(product, index) => (
// <ProductCard key={product.id} product={product} />
// )}
// />

第七阶段:打包和部署优化

Vite配置优化

vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import viteCompression from 'vite-plugin-compression';
import path from 'path';

export default defineConfig({
plugins: [
react(),
// Gzip压缩
viteCompression({
verbose: true,
disable: false,
threshold: 10240, // 只压缩大于10KB的文件
algorithm: 'gzip',
ext: '.gz',
}),

// Brotli压缩
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'brotliCompress',
ext: '.br',
}),

// 打包分析
visualizer({
open: false,
gzipSize: true,
brotliSize: true,
}),
],

resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},

build: {
// 代码分割
rollupOptions: {
output: {
manualChunks: {
// React相关
'react-vendor': ['react', 'react-dom', 'react-router-dom'],

// UI组件库
'ui-vendor': ['framer-motion'],

// 工具库
'utils': ['axios', 'date-fns'],
},

// 资源文件命名
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},

// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 删除console
drop_debugger: true, // 删除debugger
pure_funcs: ['console.log'], // 删除特定函数调用
},
},

// chunk大小警告阈值
chunkSizeWarningLimit: 1000,

// 启用CSS代码分割
cssCodeSplit: true,

// 设置构建目标
target: 'es2015',
},

// 开发服务器配置
server: {
port: 3000,
host: true,
open: true,
},

// 预加载配置
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom'],
},
});

环境变量配置

.env.development:

VITE_API_URL=http://localhost:5000/api
VITE_APP_NAME=E-Commerce App
VITE_ENABLE_DEV_TOOLS=true

.env.production:

VITE_API_URL=https://api.yourapp.com
VITE_APP_NAME=E-Commerce App
VITE_ENABLE_DEV_TOOLS=false
VITE_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX

性能监控

src/utils/performance.js:

// 性能指标收集
export const reportWebVitals = (metric) => {
const { name, value, id } = metric;

// 发送到分析服务
if (window.gtag) {
window.gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
non_interaction: true,
});
}

// 开发环境打印
if (import.meta.env.DEV) {
console.log(`[Web Vitals] ${name}:`, value);
}
};

// 监控资源加载时间
export const measureResourceTiming = () => {
if (!window.performance || !window.performance.getEntriesByType) {
return;
}

const resources = window.performance.getEntriesByType('resource');
const slowResources = resources.filter((r) => r.duration > 1000);

if (slowResources.length > 0) {
console.warn('Slow loading resources:', slowResources);
}
};

// 监控长任务
export const measureLongTasks = () => {
if (!window.PerformanceObserver) {
return;
}

const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
});
}
});

try {
observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
// longtask not supported
}
};

第八阶段:测试和调试

组件测试示例

ProductCard.test.jsx:

import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import ProductCard from './ProductCard';

const mockProduct = {
id: '1',
name: 'Test Product',
price: 99.99,
image: '/test-image.jpg',
category: 'Electronics',
rating: 4.5,
reviews: 100,
discount: 10,
};

const renderWithRouter = (component) => {
return render(<BrowserRouter>{component}</BrowserRouter>);
};

describe('ProductCard', () => {
it('renders product information correctly', () => {
renderWithRouter(<ProductCard product={mockProduct} />);

expect(screen.getByText('Test Product')).toBeInTheDocument();
expect(screen.getByText('Electronics')).toBeInTheDocument();
expect(screen.getByText('¥89.99')).toBeInTheDocument(); // 99.99 - 10%
});

it('adds to cart when button is clicked', () => {
const mockAddToCart = vi.fn();

renderWithRouter(
<ProductCard
product={mockProduct}
addToCart={mockAddToCart}
/>
);

const addToCartButton = screen.getByText(/\+ 加入/);
fireEvent.click(addToCartButton);

expect(mockAddToCart).toHaveBeenCalledWith(mockProduct);
});

it('toggles wishlist when heart icon is clicked', () => {
const mockToggleWishlist = vi.fn();

renderWithRouter(
<ProductCard
product={mockProduct}
toggleWishlist={mockToggleWishlist}
/>
);

const wishlistButton = screen.getByLabelText(/添加到收藏/);
fireEvent.click(wishlistButton);

expect(mockToggleWishlist).toHaveBeenCalledWith(mockProduct);
});
});

实战案例完整演示

完整的电商首页

src/pages/Home/Home.jsx:

import React from 'react';
import { Link } from 'react-router-dom';
import Button from '../../components/common/Button/Button';
import ProductList from '../../components/features/ProductList/ProductList';
import { productService } from '../../services/productService';
import styles from './Home.module.css';

const Home = () => {
const featuredProducts = productService.getFeatured();

return (
<div className={styles.home}>
{/* Hero Section */}
<section className={styles.hero}>
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>
发现好物<br />
<span className={styles.highlight}>品质生活</span>
</h1>
<p className={styles.heroSubtitle}>
精选全球好货,为您打造优质生活体验
</p>
<div className={styles.heroActions}>
<Button size="large" variant="primary">
立即购买
</Button>
<Button size="large" variant="outline">
了解更多
</Button>
</div>
</div>
<div className={styles.heroImage}>
<img
src="/hero-banner.jpg"
alt="Featured Products"
loading="eager"
/>
</div>
</section>

{/* Categories */}
<section className={styles.categories}>
<h2 className={styles.sectionTitle}>热门分类</h2>
<div className={styles.categoryGrid}>
{['电子数码', '服装鞋包', '家居生活', '美妆护肤'].map((category) => (
<Link
key={category}
to={`/products?category=${category}`}
className={styles.categoryCard}
>
<h3>{category}</h3>
</Link>
))}
</div>
</section>

{/* Featured Products */}
<section className={styles.featured}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>精选商品</h2>
<Link to="/products" className={styles.viewAll}>
查看全部 →
</Link>
</div>
<ProductList
fetchProducts={(params) =>
productService.getAll({ ...params, featured: true })
}
pageSize={8}
/>
</section>

{/* Newsletter */}
<section className={styles.newsletter}>
<div className={styles.newsletterContent}>
<h2>订阅我们的新闻</h2>
<p>获取最新优惠信息和产品资讯</p>
<form className={styles.newsletterForm}>
<input
type="email"
placeholder="输入您的邮箱"
className={styles.newsletterInput}
/>
<Button type="submit" variant="primary">
订阅
</Button>
</form>
</div>
</section>
</div>
);
};

export default Home;

性能优化技巧总结

1. 图片优化

✓ 使用WebP格式(比JPEG小25-35%)
✓ 响应式图片(srcset + sizes)
✓ 懒加载(IntersectionObserver)
✓ 图片压缩(tinypng.com)
✓ CDN加速
✓ BlurHash占位符

2. 代码优化

✓ 路由级别代码分割
✓ 组件级别懒加载
✓ Tree Shaking
✓ 移除未使用的代码
✓ 压缩和混淆
✓ Polyfill按需加载

3. 渲染优化

✓ React.memo防止不必要的重渲染
✓ useMemo缓存计算结果
✓ useCallback缓存函数
✓ 虚拟化长列表
✓ 防抖和节流
✓ RequestAnimationFrame优化动画

4. 网络优化

✓ HTTP/2多路复用
✓ 资源预加载
✓ DNS预解析
✓ Gzip/Brotli压缩
✓ 浏览器缓存策略
✓ CDN就近访问

常见问题解决

Q1: 如何避免内存泄漏?

// ❌ 错误示例
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
}, []);

// ✅ 正确示例
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);

return () => clearInterval(timer); // 清理副作用
}, []);

Q2: 如何优化组件重渲染?

// ✓ 使用React.memo
const ProductCard = React.memo(({ product }) => {
return <div>{product.name}</div>;
});

// ✓ 使用useMemo缓存计算
const sortedProducts = useMemo(() => {
return products.sort((a, b) => a.price - b.price);
}, [products]);

// ✓ 使用useCallback缓存函数
const handleClick = useCallback(() => {
onProductClick(product);
}, [product, onProductClick]);

Q3: 如何处理大量数据渲染?

// 方案1: 虚拟化
<VirtualList
items={largeDataArray}
itemHeight={100}
containerHeight={600}
renderItem={(item) => <ItemCard data={item} />}
/>

// 方案2: 分页加载
const [page, setPage] = useState(1);
const { data } = useAsync(() => fetchData(page), [page]);

// 方案3: 无限滚动
const [loadMoreRef, isVisible] = useIntersectionObserver();
useEffect(() => {
if (isVisible) loadMore();
}, [isVisible]);

学习成果检验

完成这个实战后,你应该能够:

✅ 创建可复用的组件库 ✅ 实现复杂的状态管理 ✅ 开发自定义Hooks ✅ 优化应用性能 ✅ 配置生产级打包 ✅ 实现响应式设计 ✅ 处理异步操作 ✅ 进行单元测试

进阶方向

  1. 服务端渲染(SSR)

    • 迁移到Next.js
    • 实现SSG和ISR
    • SEO优化
  2. PWA应用

    • Service Worker
    • 离线缓存
    • 推送通知
  3. 微前端架构

    • 模块联邦
    • 独立部署
    • 状态共享
  4. 可视化开发

    • 拖拽式构建器
    • 低代码平台
    • 动态表单

总结

前端开发远不止是创建漂亮的界面。通过这个实战案例,我们学习了:

  1. 组件化思维: 将UI拆分为可复用的组件
  2. 状态管理: 使用Context和Hooks管理复杂状态
  3. 性能优化: 从代码分割到图片优化全方位提升
  4. 用户体验: 加载状态、错误处理、平滑动画
  5. 工程化: 打包优化、自动化测试、持续部署

Claude Code能够显著加速这些开发流程,让你专注于业务逻辑和用户体验,而不是被重复的样板代码所困扰。

结合AI辅助开发,前端开发的效率可以提升8-10倍。这正是现代前端开发的未来方向!

下一步学习