引言:为什么需要完整的React开发流程?
React作为目前最流行的前端框架之一,其生态系统庞大且复杂。许多开发者在入门时往往只关注组件编写,而忽略了完整的项目开发流程。本文将从零开始,详细讲解一个React项目的完整生命周期,包括项目初始化、架构设计、开发实践、测试部署等各个环节,并分享业界最佳实践。
第一阶段:项目初始化与工具链选择
1.1 现代React项目创建方式
传统方式 vs 现代方式
早期的React项目通常使用
create-react-app
(CRA)作为脚手架工具。然而,随着前端工程化的发展,现代React项目更倾向于使用
Vite
或
Next.js
。
为什么选择Vite?
极速冷启动
:利用ESM原生支持,启动速度比Webpack快10-100倍
热更新(HMR)性能
:模块热更新速度与模块总数无关
开箱即用
:内置TypeScript、JSX、CSS预处理等支持
实战:使用Vite创建React项目
# 使用npm
npm create vite@latest my-react-app — –template react-ts
# 使用yarn
yarn create vite my-react-app –template react-ts
# 使用pnpm(推荐)
pnpm create vite my-react-app –template react-ts
项目结构预览:
my-react-app/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 图片、字体等
│ ├── components/ # 公共组件
│ ├── hooks/ # 自定义hooks
│ ├── pages/ # 页面组件
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.tsx # 根组件
│ └── main.tsx # 入口文件
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json # TypeScript配置
└── vite.config.ts # Vite配置
1.2 包管理器的选择与配置
推荐使用pnpm
,原因如下:
磁盘空间高效
:通过硬链接和符号链接共享依赖
安装速度快
:并行下载依赖
严格的依赖管理
:避免幽灵依赖
# 初始化pnpm
pnpm init
# 安装React核心依赖
pnpm add react react-dom
# 安装开发依赖
pnpm add -D typescript @types/react @types/react-dom
pnpm add -D eslint prettier husky lint-staged
1.3 TypeScript配置最佳实践
创建
tsconfig.json
:
{
“compilerOptions”: {
“target”: “ES2020”,
“useDefineForClassFields”: true,
“lib”: [“ES2020”, “DOM”, “DOM.Iterable”],
“module”: “ESNext”,
“skipLibCheck”: true,
/* 模块解析 */
“moduleResolution”: “bundler”,
“allowImportingTsExtensions”: true,
“resolveJsonModule”: true,
“isolatedModules”: true,
“noEmit”: true,
“jsx”: “react-jsx”,
/* 严格模式 */
“strict”: true,
“noUnusedLocals”: true,
“noUnusedParameters”: true,
“exactOptionalPropertyTypes”: true,
/* 导入别名 */
“baseUrl”: “.”,
“paths”: {
“@/*”: [“./src/*”]
},
/* 类型检查 */
“allowJs”: false,
“forceConsistentCasingInFileNames”: true,
“noImplicitReturns”: true,
“noFallthroughCasesInSwitch”: true
},
“include”: [“src”],
“references”: [{ “path”: “./tsconfig.node.json” }]
}
1.4 ESLint + Prettier代码规范配置
安装依赖:
pnpm add -D eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
pnpm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier
ESLint配置(.eslintrc.cjs):
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
‘eslint:recommended’,
‘plugin:@typescript-eslint/recommended’,
‘plugin:react/recommended’,
‘plugin:react-hooks/recommended’,
‘plugin:jsx-a11y/recommended’,
‘prettier’
],
ignorePatterns: [‘dist’, ‘.eslintrc.cjs’],
parser: ‘@typescript-eslint/parser’,
plugins: [‘react-refresh’, ‘simple-import-sort’],
rules: {
‘react-refresh/only-export-components’: [
‘warn’,
{ allowConstantExport: true },
],
// 严格模式
‘@typescript-eslint/no-explicit-any’: ‘error’,
‘@typescript-eslint/no-unused-vars’: ‘warn’,
‘react/react-in-jsx-scope’: ‘off’, // React 17+ 不需要
‘react/prop-types’: ‘off’, // 使用TypeScript
// 导入排序
‘simple-import-sort/imports’: ‘error’,
‘simple-import-sort/exports’: ‘error’,
// 代码风格
‘prettier/prettier’: ‘error’,
},
settings: {
react: {
version: ‘detect’,
},
},
}
Prettier配置(.prettierrc):
{
“semi”: true,
“trailingComma”: “es5”,
“singleQuote”: true,
“printWidth”: 80,
“tabWidth”: 2,
“useTabs”: false,
“endOfLine”: “lf”
}
1.5 Git Hooks与提交规范
配置Husky + lint-staged:
# 初始化Husky
pnpm add -D husky
npx husky init
pnpm set-script prepare “husky install”
# 添加pre-commit钩子
npx husky add .husky/pre-commit “pnpm lint-staged”
# 添加commit-msg钩子(用于提交信息规范)
npx husky add .husky/commit-msg “npx –no-install commitlint –edit $1”
配置lint-staged(package.json):
{
“lint-staged”: {
“*.{js,jsx,ts,tsx}”: [
“eslint –fix”,
“prettier –write”
],
“*.{json,css,md}”: [
“prettier –write”
]
}
}
配置Commitizen(提交信息规范):
pnpm add -D commitizen cz-conventional-changelog
# package.json
{
“scripts”: {
“commit”: “git-cz”
},
“config”: {
“commitizen”: {
“path”: “node_modules/cz-conventional-changelog”
}
}
}
第二阶段:项目架构设计
2.1 目录结构设计原则
推荐的目录结构:
src/
├── assets/ # 静态资源(不经常变动)
├── components/ # 公共组件
│ ├── ui/ # 基础UI组件(Button, Input等)
│ ├── layout/ # 布局组件(Header, Sidebar等)
│ └── __tests__/ # 组件测试
├── pages/ # 页面组件(路由级别)
├── hooks/ # 自定义Hooks
│ ├── useAuth.ts
│ └── useApi.ts
├── store/ # 状态管理
│ ├── index.ts
│ ├── slices/
│ └── types.ts
├── services/ # API服务层
│ ├── api.ts
│ └── endpoints/
├── utils/ # 工具函数
│ ├── helpers.ts
│ ├── validators.ts
│ └── constants.ts
├── types/ # 全局类型定义
├── styles/ # 全局样式
├── routes/ # 路由配置
├── App.tsx # 根组件
└── main.tsx # 入口文件
2.2 路由设计与管理
推荐使用React Router v6:
pnpm add react-router-dom
路由配置(routes/index.tsx):
import { createBrowserRouter, RouterProvider } from ‘react-router-dom’;
import { lazy, Suspense } from ‘react’;
import AppLayout from ‘@/components/layout/AppLayout’;
import Loading from ‘@/components/ui/Loading’;
// 懒加载页面组件
const Home = lazy(() => import(‘@/pages/Home’));
const Dashboard = lazy(() => import(‘@/pages/Dashboard’));
const Profile = lazy(() => import(‘@/pages/Profile’));
const NotFound = lazy(() => import(‘@/pages/NotFound’));
// 路由守卫组件
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const { user } = useAuth(); // 自定义认证hook
if (!user) {
return
}
return children;
};
const router = createBrowserRouter([
{
path: ‘/’,
element: (
),
children: [
{
index: true,
element:
},
{
path: ‘dashboard’,
element: (
),
},
{
path: ‘profile’,
element: (
),
},
],
},
{
path: ‘*’,
element:
},
]);
export default function AppRouter() {
return
}
2.3 状态管理方案选择
状态管理决策树:
组件内状态 → useReducer + Context → Zustand → Redux Toolkit → MobX
(简单) (中等) (推荐) (复杂) (特定场景)
推荐方案:Zustand(简单且强大)
pnpm add zustand
pnpm add -D @types/node
创建Store(store/useUserStore.ts):
import { create } from ‘zustand’;
import { devtools, persist } from ‘zustand/middleware’;
interface UserState {
user: { id: string; name: string; email: string } | null;
token: string | null;
isLoading: boolean;
error: string | null;
// Actions
login: (email: string, password: string) => Promise
logout: () => void;
updateUser: (data: Partial
clearError: () => void;
}
export const useUserStore = create
devtools(
persist(
(set, get) => ({
user: null,
token: null,
isLoading: false,
error: null,
login: async (email, password) => {
set({ isLoading: true, error: null });
try {
// 模拟API调用
const response = await fetch(‘/api/auth/login’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error(‘登录失败’);
const data = await response.json();
set({
user: data.user,
token: data.token,
isLoading: false
});
} catch (error) {
set({
error: error instanceof Error ? error.message : ‘未知错误’,
isLoading: false
});
}
},
logout: () => {
set({ user: null, token: null, error: null });
},
updateUser: (data) => {
set((state) => ({
user: state.user ? { …state.user, …data } : null,
}));
},
clearError: () => set({ error: null }),
}),
{
name: ‘user-storage’, // localStorage key
partialize: (state) => ({
user: state.user,
token: state.token
}), // 只持久化特定字段
}
)
)
);
在组件中使用:
import { useUserStore } from ‘@/store/useUserStore’;
function LoginForm() {
const { login, isLoading, error, clearError } = useUserStore();
const [form, setForm] = useState({ email: ”, password: ” });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(form.email, form.password);
};
return (
);
}
2.4 API服务层封装
统一的API客户端(services/api.ts):
import axios, {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from ‘axios’;
import { useUserStore } from ‘@/store/useUserStore’;
// 创建axios实例
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || ‘/api’,
timeout: 10000,
headers: {
‘Content-Type’: ‘application/json’,
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = useUserStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
// 成功响应处理
return response.data;
},
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// Token过期处理(401)
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 尝试刷新token
const refreshToken = useUserStore.getState().token;
const response = await axios.post(‘/api/auth/refresh’, { refreshToken });
const newToken = response.data.token;
useUserStore.getState().updateUser({ token: newToken });
// 重试原请求
originalRequest.headers = {
…originalRequest.headers,
Authorization: `Bearer ${newToken}`,
};
return apiClient(originalRequest);
} catch (refreshError) {
// 刷新失败,退出登录
useUserStore.getState().logout();
window.location.href = ‘/login’;
return Promise.reject(refreshError);
}
}
// 其他错误处理
const message = error.response?.data?.message || error.message;
console.error(‘API Error:’, message);
return Promise.reject(new Error(message));
}
);
// 封装常用请求方法
export const api = {
get:
apiClient.get
post:
apiClient.post
put:
apiClient.put
delete:
apiClient.delete
};
// 类型安全的API调用示例
interface UserResponse {
id: string;
name: string;
email: string;
}
export const getUserProfile = () => api.get
export const updateUserProfile = (data: Partial
api.put
2.5 自定义Hooks封装
API调用Hook(hooks/useApi.ts):
import { useState, useCallback } from ‘react’;
interface ApiState
data: T | null;
loading: boolean;
error: string | null;
}
export function useApi
const [state, setState] = useState
data: null,
loading: false,
error: null,
});
const execute = useCallback(
async (apiCall: () => Promise
setState((prev) => ({ …prev, loading: true, error: null }));
try {
const data = await apiCall();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
const message = error instanceof Error ? error.message : ‘请求失败’;
setState({ data: null, loading: false, error: message });
return null;
}
},
[]
);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
return { …state, execute, reset };
}
// 使用示例
function UserProfile() {
const { data, loading, error, execute } = useApi
const { updateUser } = useUserStore();
const loadProfile = useCallback(() => {
execute(getUserProfile).then((data) => {
if (data) updateUser(data);
});
}, [execute, updateUser]);
if (loading) return
;
if (error) return
;
return (
{data?.name}
);
}
第三阶段:组件开发最佳实践
3.1 组件设计原则
原子设计方法论:
Atoms(原子)
:基础组件(Button, Input, Icon)
Molecules(分子)
:组合原子(SearchForm = Input + Button)
Organisms(有机体)
:组合分子(Header = Logo + SearchForm + UserMenu)
Templates(模板)
:页面布局结构
Pages(页面)
:最终页面
3.2 基础组件示例:可复用Button组件
// components/ui/Button.tsx
import { ButtonHTMLAttributes, forwardRef, ReactNode } from ‘react’;
import { cva, type VariantProps } from ‘class-variance-authority’;
import { cn } from ‘@/utils/cn’;
// 使用class-variance-authority定义按钮变体
const buttonVariants = cva(
‘inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none’,
{
variants: {
variant: {
default: ‘bg-primary text-primary-foreground hover:bg-primary/90’,
destructive: ‘bg-destructive text-destructive-foreground hover:bg-destructive/90’,
outline: ‘border border-input hover:bg-accent hover:text-accent-foreground’,
secondary: ‘bg-secondary text-secondary-foreground hover:bg-secondary/80’,
ghost: ‘hover:bg-accent hover:text-accent-foreground’,
link: ‘text-primary underline-offset-4 hover:underline’,
},
size: {
default: ‘h-10 px-4 py-2’,
sm: ‘h-9 rounded-md px-3’,
lg: ‘h-11 rounded-md px-8’,
icon: ‘h-10 w-10’,
},
},
defaultVariants: {
variant: ‘default’,
size: ‘default’,
},
}
);
export interface ButtonProps
extends ButtonHTMLAttributes
VariantProps
children: ReactNode;
isLoading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
}
const Button = forwardRef
(
{
className,
variant,
size,
isLoading,
leftIcon,
rightIcon,
children,
disabled,
…props
},
ref
) => {
return (
);
}
);
Button.displayName = ‘Button’;
export { Button, buttonVariants };
使用示例:
import { Button } from ‘@/components/ui/Button’;
import { Icons } from ‘@/components/ui/Icons’;
function Example() {
return (
}>
新增
);
}
3.3 复杂组件示例:数据表格组件
// components/ui/DataTable.tsx
import { useState, useMemo } from ‘react’;
interface Column
key: keyof T;
title: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
sortable?: boolean;
}
interface DataTableProps
data: T[];
columns: Column
onRowClick?: (row: T) => void;
initialSort?: { key: keyof T; direction: ‘asc’ | ‘desc’ };
}
export function DataTable
data,
columns,
onRowClick,
initialSort,
}: DataTableProps
const [sortConfig, setSortConfig] = useState<{
key: keyof T;
direction: 'asc' | 'desc';
} | null>(initialSort || null);
const sortedData = useMemo(() => {
if (!sortConfig) return data;
return […data].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal === bVal) return 0;
const comparison = aVal > bVal ? 1 : -1;
return sortConfig.direction === ‘asc’ ? comparison : -comparison;
});
}, [data, sortConfig]);
const handleSort = (key: keyof T) => {
setSortConfig((prev) => {
if (!prev || prev.key !== key) {
return { key, direction: ‘asc’ };
}
if (prev.direction === ‘asc’) {
return { key, direction: ‘desc’ };
}
return null;
});
};
return (
|
{col.title}
{col.sortable && ( )} |
|---|
|
{col.render ? col.render(row[col.key], row) : String(row[col.key])} |
);
}
使用示例:
interface User {
id: string;
name: string;
email: string;
role: string;
status: ‘active’ | ‘inactive’;
}
function UserTable() {
const columns: Column
{ key: ‘name’, title: ‘姓名’, sortable: true },
{ key: ’email’, title: ‘邮箱’, sortable: true },
{
key: ‘role’,
title: ‘角色’,
render: (value) => (
{value}
),
},
{
key: ‘status’,
title: ‘状态’,
render: (value) => (
{value === ‘active’ ? ‘活跃’ : ‘禁用’}
),
},
];
const users: User[] = [
{ id: ‘1’, name: ‘张三’, email: ‘zhang@example.com’, role: ‘admin’, status: ‘active’ },
{ id: ‘2’, name: ‘李四’, email: ‘li@example.com’, role: ‘user’, status: ‘inactive’ },
];
return (
initialSort={{ key: ‘name’, direction: ‘asc’ }}
/>
);
}
3.4 组件性能优化
使用React.memo避免不必要的重渲染:
import { memo, useState, useCallback } from ‘react’;
interface ExpensiveChildProps {
count: number;
onClick: () => void;
}
// 只有当props变化时才重新渲染
const ExpensiveChild = memo(({ count, onClick }: ExpensiveChildProps) => {
console.log(‘ExpensiveChild rendered’);
return (
Count: {count}
);
});
// 使用useCallback保持函数引用稳定
function Parent() {
const [count, setCount] = useState(0);
const [unrelated, setUnrelated] = useState(0);
// 错误:每次渲染都会创建新函数
// const handleClick = () => setCount(c => c + 1);
// 正确:使用useCallback保持引用稳定
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
);
}
使用useMemo缓存计算结果:
import { useMemo } from ‘react’;
function ExpensiveList({ items, filter }: { items: string[]; filter: string }) {
// 避免每次渲染都重新过滤和排序
const filteredItems = useMemo(() => {
console.log(‘执行昂贵计算’);
return items
.filter(item => item.includes(filter))
.sort((a, b) => a.localeCompare(b));
}, [items, filter]);
return (
-
{filteredItems.map(item => (
- {item}
))}
);
}
第四阶段:样式与主题管理
4.1 CSS-in-JS vs CSS Modules vs Tailwind
对比分析:
方案
优点
缺点
适用场景
CSS Modules
作用域隔离、编译时优化
需要额外配置、不支持动态样式
中小型项目
Tailwind
开发速度快、体积小、响应式友好
HTML冗长、学习曲线
快速原型、设计系统
CSS-in-JS
动态样式、主题支持、TS友好
运行时开销、包体积大
复杂应用、主题切换
推荐:Tailwind + CSS Modules混合方案
4.2 Tailwind配置与最佳实践
安装:
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
配置(tailwind.config.js):
/** @type {import(‘tailwindcss’).Config} */
export default {
content: [
“./index.html”,
“./src/**/*.{js,ts,jsx,tsx}”,
],
theme: {
extend: {
colors: {
primary: {
50: ‘#eff6ff’,
100: ‘#dbeafe’,
500: ‘#3b82f6’,
600: ‘#2563eb’,
700: ‘#1d4ed8′,
},
},
spacing: {
’18’: ‘4.5rem’,
},
animation: {
‘fade-in’: ‘fadeIn 0.3s ease-in-out’,
},
keyframes: {
fadeIn: {
‘0%’: { opacity: ‘0’, transform: ‘translateY(10px)’ },
‘100%’: { opacity: ‘1’, transform: ‘translateY(0)’ },
},
},
},
},
plugins: [],
}
全局样式(src/index.css):
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
–background: 0 0% 100%;
–foreground: 222.2 84% 4.9%;
/* 更多CSS变量 */
}
body {
@apply bg-background text-foreground;
font-family: ‘Inter’, system-ui, sans-serif;
}
}
@layer components {
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700 transition-colors;
}
.card {
@apply rounded-lg border bg-card text-card-foreground shadow-sm;
}
}
4.3 主题切换实现
使用CSS变量 + Context:
// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from ‘react’;
type Theme = ‘light’ | ‘dark’ | ‘system’;
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: ‘light’ | ‘dark’;
}
const ThemeContext = createContext
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(‘light’);
useEffect(() => {
// 从localStorage读取
const saved = localStorage.getItem(‘theme’) as Theme;
if (saved) setTheme(saved);
}, []);
useEffect(() => {
// 解析system主题
if (theme === ‘system’) {
const media = window.matchMedia(‘(prefers-color-scheme: dark)’);
setResolvedTheme(media.matches ? ‘dark’ : ‘light’);
const listener = (e: MediaQueryListEvent) => {
setResolvedTheme(e.matches ? ‘dark’ : ‘light’);
};
media.addEventListener(‘change’, listener);
return () => media.removeEventListener(‘change’, listener);
} else {
setResolvedTheme(theme);
}
}, [theme]);
useEffect(() => {
// 应用到HTML元素
document.documentElement.setAttribute(‘data-theme’, resolvedTheme);
document.documentElement.classList.toggle(‘dark’, resolvedTheme === ‘dark’);
localStorage.setItem(‘theme’, theme);
}, [theme, resolvedTheme]);
return (
{children}
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error(‘useTheme must be used within ThemeProvider’);
return context;
}
主题切换组件:
// components/ui/ThemeToggle.tsx
import { useTheme } from ‘@/contexts/ThemeContext’;
import { Icons } from ‘@/components/ui/Icons’;
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const toggle = () => {
setTheme(theme === ‘light’ ? ‘dark’ : theme === ‘dark’ ? ‘system’ : ‘light’);
};
const Icon = theme === ‘light’ ? Icons.sun : theme === ‘dark’ ? Icons.moon : Icons.laptop;
return (
);
}
第五阶段:状态管理进阶
5.1 异步状态管理
使用Zustand处理异步操作:
// store/useProductStore.ts
import { create } from ‘zustand’;
import { devtools } from ‘zustand/middleware’;
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
interface ProductState {
products: Product[];
loading: boolean;
error: string | null;
page: number;
hasMore: boolean;
fetchProducts: (page?: number) => Promise
loadMore: () => Promise
updateStock: (id: string, stock: number) => Promise
}
export const useProductStore = create
devtools((set, get) => ({
products: [],
loading: false,
error: null,
page: 1,
hasMore: true,
fetchProducts: async (page = 1) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/products?page=${page}&limit=10`);
if (!response.ok) throw new Error(‘获取产品失败’);
const data = await response.json();
set({
products: page === 1 ? data.items : […get().products, …data.items],
page,
hasMore: data.hasMore,
loading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : ‘未知错误’,
loading: false,
});
}
},
loadMore: async () => {
const { page, hasMore, fetchProducts } = get();
if (hasMore && !get().loading) {
await fetchProducts(page + 1);
}
},
updateStock: async (id: string, stock: number) => {
try {
const response = await fetch(`/api/products/${id}/stock`, {
method: ‘PATCH’,
body: JSON.stringify({ stock }),
});
if (!response.ok) throw new Error(‘更新失败’);
set({
products: get().products.map(p =>
p.id === id ? { …p, stock } : p
),
});
} catch (error) {
set({
error: error instanceof Error ? error.message : ‘更新失败’,
});
throw error;
}
},
}))
);
5.2 数据缓存与优化
实现简单的缓存机制:
// hooks/useCachedApi.ts
import { useCallback, useRef } from ‘react’;
import { useApi } from ‘./useApi’;
interface CacheEntry
data: T;
timestamp: number;
expiry: number;
}
export function useCachedApi
const cacheRef = useRef
const cachedExecute = useCallback(
async (apiCall: () => Promise
const now = Date.now();
const cached = cacheRef.current.get(cacheKey);
// 返回缓存数据
if (cached && now – cached.timestamp < ttl) {
return cached.data;
}
// 重新获取
const result = await execute(apiCall);
if (result) {
cacheRef.current.set(cacheKey, {
data: result,
timestamp: now,
expiry: now + ttl,
});
}
return result;
},
[cacheKey, ttl, execute]
);
const clearCache = useCallback(() => {
cacheRef.current.delete(cacheKey);
reset();
}, [cacheKey, reset]);
return { data, loading, error, execute: cachedExecute, clearCache };
}
5.3 全局状态监听与持久化
高级持久化中间件:
// store/middleware/persistWithEncryption.ts
import { StateCreator } from ‘zustand’;
export function persistWithEncryption
config: StateCreator
options: {
name: string;
partialize?: (state: T) => Partial
encrypt?: (data: string) => string;
decrypt?: (data: string) => string;
}
) {
const { name, partialize, encrypt, decrypt } = options;
return ((set, get, api) => {
const persisted = (() => {
try {
const item = localStorage.getItem(name);
if (!item) return null;
const decrypted = decrypt ? decrypt(item) : item;
return JSON.parse(decrypted);
} catch (e) {
console.error(‘Failed to load persisted state:’, e);
return null;
}
})();
const state = config(
(args) => {
set(args);
// 保存到localStorage
try {
const stateToPersist = partialize ? partialize(get()) : get();
const serialized = JSON.stringify(stateToPersist);
const encrypted = encrypt ? encrypt(serialized) : serialized;
localStorage.setItem(name, encrypted);
} catch (e) {
console.error(‘Failed to persist state:’, e);
}
},
get,
api
);
return { …state, …(persisted || {}) };
}) as StateCreator
}
第六阶段:测试策略
6.1 测试金字塔与工具选择
/\
/E2E\ Cypress, Playwright
/——\
/Integr \ React Testing Library
/———\
/ Unit \ Jest + Vitest
/————-\
6.2 单元测试示例
安装测试工具:
pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom
配置(vitest.config.ts):
import { defineConfig } from ‘vitest/config’;
import react from ‘@vitejs/plugin-react’;
export default defineConfig({
plugins: [react()],
test: {
environment: ‘jsdom’,
setupFiles: ‘./src/test/setup.ts’,
globals: true,
},
});
测试示例(Button.test.tsx):
import { render, screen, fireEvent } from ‘@testing-library/react’;
import { describe, it, expect, vi } from ‘vitest’;
import { Button } from ‘./Button’;
describe(‘Button Component’, () => {
it(‘renders children correctly’, () => {
render();
expect(screen.getByText(‘Click me’)).toBeInTheDocument();
});
it(‘handles click events’, () => {
const handleClick = vi.fn();
render();
fireEvent.click(screen.getByText(‘Click’));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it(‘is disabled when loading’, () => {
render();
const button = screen.getByText(‘Submit’);
expect(button).toBeDisabled();
});
it(‘shows loading spinner’, () => {
render();
expect(screen.getByRole(‘status’)).toBeInTheDocument();
});
});
6.3 集成测试示例
用户登录流程测试:
// __tests__/LoginFlow.test.tsx
import { render, screen, waitFor } from ‘@testing-library/react’;
import userEvent from ‘@testing-library/user-event’;
import { describe, it, expect, vi, beforeEach } from ‘vitest’;
import { QueryClient, QueryClientProvider } from ‘@tanstack/react-query’;
import LoginForm from ‘@/pages/Login’;
// Mock API
vi.mock(‘@/services/api’, () => ({
api: {
post: vi.fn(),
},
}));
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
);
};
describe(‘Login Flow’, () => {
beforeEach(() => {
vi.clearAllMocks();
});
it(‘successful login redirects to dashboard’, async () => {
const user = userEvent.setup();
const mockPost = vi.mocked(await import(‘@/services/api’)).api.post;
mockPost.mockResolvedValueOnce({ token: ‘fake-token’, user: { id: ‘1’, name: ‘Test’ } });
render(
await user.type(screen.getByLabelText(/email/i), ‘test@example.com’);
await user.type(screen.getByLabelText(/password/i), ‘password123’);
await user.click(screen.getByRole(‘button’, { name: /login/i }));
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith(‘/auth/login’, {
email: ‘test@example.com’,
password: ‘password123’,
});
});
});
});
6.4 E2E测试(Playwright)
安装:
pnpm add -D @playwright/test
npx playwright install
测试示例(e2e/login.spec.ts):
import { test, expect } from ‘@playwright/test’;
test.describe(‘Login Page’, () => {
test(‘should login successfully’, async ({ page }) => {
await page.goto(‘http://localhost:5173/login’);
// 填写表单
await page.fill(‘input[name=”email”]’, ‘test@example.com’);
await page.fill(‘input[name=”password”]’, ‘password123’);
await page.click(‘button[type=”submit”]’);
// 验证跳转
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator(‘h1’)).toContainText(‘Dashboard’);
});
test(‘should show error on invalid credentials’, async ({ page }) => {
await page.goto(‘http://localhost:5173/login’);
await page.fill(‘input[name=”email”]’, ‘wrong@example.com’);
await page.fill(‘input[name=”password”]’, ‘wrongpass’);
await page.click(‘button[type=”submit”]’);
await expect(page.locator(‘.error-banner’)).toContainText(‘登录失败’);
});
});
第七阶段:性能优化
7.1 代码分割与懒加载
路由级懒加载(已展示)
组件级懒加载:
import { lazy, Suspense } from ‘react’;
// 懒加载重型组件
const HeavyChart = lazy(() => import(‘@/components/charts/HeavyChart’));
const RichTextEditor = lazy(() => import(‘@/components/editor/RichTextEditor’));
function Dashboard() {
return (
}>