Files
ocean_project_manager/frontend/docs/state-management.md
T
2026-01-25 15:05:03 +08:00

959 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 状态管理设计文档
## 1. 状态管理方案选择
### 1.1 为什么选择Context API
对于本项目的需求,React Context API是最佳选择:
**优势:**
- 轻量级,无需引入额外的库
- React官方内置,学习成本低
- 适合中小型应用的状态管理
- 与React生态系统完美集成
**适用场景:**
- 认证状态(用户信息、Token
- 全局配置(主题、语言)
- 简单的共享状态
**不适用场景:**
- 复杂的跨组件通信
- 高频率的状态更新
- 需要时间旅行调试
- 如果将来需要,可以迁移到Redux或Zustand
### 1.2 状态管理架构
```
┌─────────────────────────────────────────┐
│ App Root │
├─────────────────────────────────────────┤
│ AuthProvider (认证状态) │
│ ├─ user (用户信息) │
│ ├─ token (认证令牌) │
│ └─ authState (认证状态) │
│ │
│ ThemeProvider (主题状态,可选) │
│ └─ theme (主题配置) │
│ │
│ Router (路由状态) │
│ └─ currentPath (当前路径) │
└─────────────────────────────────────────┘
```
## 2. Context实现
### 2.1 AuthContext(认证上下文)
```javascript
// contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { authAPI } from '../services/auth';
import { message } from 'antd';
const AuthContext = createContext(null);
/**
* 认证上下文Provider
*/
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
const [authState, setAuthState] = useState({
isAuthenticated: false,
isLoading: true,
});
/**
* 初始化认证状态
*/
useEffect(() => {
checkAuth();
}, []);
/**
* 检查登录状态
*/
const checkAuth = async () => {
try {
const savedToken = localStorage.getItem('token');
const savedUser = localStorage.getItem('user');
if (savedToken && savedUser) {
// 验证Token有效性
const currentUser = await authAPI.getCurrentUser();
setToken(savedToken);
setUser(currentUser);
setAuthState({
isAuthenticated: true,
isLoading: false,
});
} else {
clearAuth();
}
} catch (error) {
console.error('Auth check failed:', error);
clearAuth();
} finally {
setLoading(false);
}
};
/**
* 登录
*/
const login = async (username, password) => {
try {
const response = await authAPI.login(username, password);
const { token: newToken, user: userData } = response;
// 保存到localStorage
localStorage.setItem('token', newToken);
localStorage.setItem('user', JSON.stringify(userData));
// 更新状态
setToken(newToken);
setUser(userData);
setAuthState({
isAuthenticated: true,
isLoading: false,
});
return userData;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
};
/**
* 登出
*/
const logout = useCallback(async () => {
try {
await authAPI.logout();
} catch (error) {
console.error('Logout failed:', error);
} finally {
clearAuth();
}
}, []);
/**
* 清除认证信息
*/
const clearAuth = useCallback(() => {
localStorage.removeItem('token');
localStorage.removeItem('user');
setToken(null);
setUser(null);
setAuthState({
isAuthenticated: false,
isLoading: false,
});
}, []);
/**
* 更新用户信息
*/
const updateUser = useCallback((userData) => {
setUser((prev) => ({ ...prev, ...userData }));
localStorage.setItem('user', JSON.stringify({ ...user, ...userData }));
}, [user]);
/**
* 检查权限
*/
const hasRole = useCallback(
(roles) => {
if (!user) return false;
if (Array.isArray(roles)) {
return roles.includes(user.role);
}
return user.role === roles;
},
[user]
);
/**
* 检查是否可以编辑
*/
const canEdit = useCallback(
(createdBy) => {
if (!user) return false;
if (user.role === 'admin') return true;
if (user.role === 'market' && createdBy === user.id) return true;
return false;
},
[user]
);
const value = {
user,
token,
loading,
authState,
login,
logout,
checkAuth,
updateUser,
hasRole,
canEdit,
isAuthenticated: authState.isAuthenticated,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
/**
* 使用AuthContext的Hook
*/
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
export default AuthContext;
```
### 2.2 ThemeContext(主题上下文,可选)
```javascript
// contexts/ThemeContext.jsx
import { createContext, useContext, useState, useCallback, useEffect } from 'react';
const ThemeContext = createContext(null);
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
// 从localStorage加载主题
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}
}, []);
/**
* 切换主题
*/
const toggleTheme = useCallback(() => {
setTheme((prev) => {
const newTheme = prev === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
return newTheme;
});
}, []);
/**
* 设置主题
*/
const setThemeMode = useCallback((mode) => {
setTheme(mode);
localStorage.setItem('theme', mode);
document.documentElement.setAttribute('data-theme', mode);
}, []);
const value = {
theme,
toggleTheme,
setThemeMode,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
export default ThemeContext;
```
## 3. 自定义Hooks
### 3.1 useAuth Hook
```javascript
// hooks/useAuth.js
import { useAuth as useAuthContext } from '../contexts/AuthContext';
export const useAuth = () => {
return useAuthContext();
};
```
### 3.2 usePermission Hook
```javascript
// hooks/usePermission.js
import { useAuth } from './useAuth';
/**
* 权限检查Hook
*/
export const usePermission = () => {
const { user, hasRole, canEdit } = useAuth();
/**
* 检查是否是管理员
*/
const isAdmin = () => {
return user?.role === 'admin';
};
/**
* 检查是否是市场部
*/
const isMarket = () => {
return user?.role === 'market';
};
/**
* 检查是否是其他部门
*/
const isOther = () => {
return user?.role === 'other';
};
/**
* 检查是否有指定权限
*/
const hasPermission = (permission) => {
// 可以扩展更复杂的权限系统
const permissions = {
admin: ['create_user', 'edit_user', 'delete_user', 'create_project', 'edit_project', 'delete_project'],
market: ['create_project', 'edit_own_project', 'delete_own_project'],
other: ['edit_project_cost', 'edit_project_finance', 'edit_project_progress'],
};
return permissions[user?.role]?.includes(permission) || false;
};
return {
isAdmin,
isMarket,
isOther,
hasRole,
canEdit,
hasPermission,
user,
};
};
```
### 3.3 useModal Hook
```javascript
// hooks/useModal.js
import { useState, useCallback } from 'react';
/**
* 弹窗管理Hook
*/
export const useModal = () => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const open = useCallback((modalData) => {
setData(modalData);
setVisible(true);
}, []);
const close = useCallback(() => {
setVisible(false);
setData(null);
setLoading(false);
}, []);
const toggle = useCallback(() => {
setVisible((prev) => !prev);
}, []);
const setLoadingState = useCallback((state) => {
setLoading(state);
}, []);
return {
visible,
loading,
data,
open,
close,
toggle,
setLoading: setLoadingState,
};
};
```
### 3.4 useTable Hook
```javascript
// hooks/useTable.js
import { useState, useCallback } from 'react';
/**
* 表格管理Hook
*/
export const useTable = (initialState = {}) => {
const {
pagination: initialPagination = { current: 1, pageSize: 10 },
filters: initialFilters = {},
sorter: initialSorter = {},
selectedRowKeys: initialSelectedRowKeys = [],
} = initialState;
const [pagination, setPagination] = useState(initialPagination);
const [filters, setFilters] = useState(initialFilters);
const [sorter, setSorter] = useState(initialSorter);
const [selectedRowKeys, setSelectedRowKeys] = useState(initialSelectedRowKeys);
const handleTableChange = useCallback((newPagination, newFilters, newSorter) => {
setPagination(newPagination);
setFilters(newFilters);
setSorter({
field: newSorter.field,
order: newSorter.order,
});
}, []);
const resetTable = useCallback(() => {
setPagination(initialPagination);
setFilters(initialFilters);
setSorter(initialSorter);
setSelectedRowKeys(initialSelectedRowKeys);
}, [initialPagination, initialFilters, initialSorter, initialSelectedRowKeys]);
return {
pagination,
filters,
sorter,
selectedRowKeys,
setSelectedRowKeys,
handleTableChange,
resetTable,
};
};
```
### 3.5 useForm Hook
```javascript
// hooks/useForm.js
import { useState, useCallback } from 'react';
/**
* 表单管理Hook
*/
export const useForm = (initialValues = {}) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [submitting, setSubmitting] = useState(false);
/**
* 更新表单值
*/
const handleChange = useCallback((field, value) => {
setValues((prev) => ({
...prev,
[field]: value,
}));
// 清除该字段的错误
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: null,
}));
}
}, [errors]);
/**
* 批量更新表单值
*/
const setFieldsValue = useCallback((newValues) => {
setValues((prev) => ({
...prev,
...newValues,
}));
}, []);
/**
* 标记字段为已触摸
*/
const handleBlur = useCallback((field) => {
setTouched((prev) => ({
...prev,
[field]: true,
}));
}, []);
/**
* 设置错误
*/
const setFieldError = useCallback((field, error) => {
setErrors((prev) => ({
...prev,
[field]: error,
}));
}, []);
/**
* 批量设置错误
*/
const setErrorsAll = useCallback((newErrors) => {
setErrors(newErrors);
}, []);
/**
* 验证表单
*/
const validate = useCallback((validationRules) => {
const newErrors = {};
let isValid = true;
Object.keys(validationRules).forEach((field) => {
const rules = validationRules[field];
const value = values[field];
for (const rule of rules) {
let error = null;
if (rule.required && !value) {
error = rule.message || `${field} is required`;
} else if (rule.pattern && !rule.pattern.test(value)) {
error = rule.message || `${field} is invalid`;
} else if (rule.validator && !rule.validator(value)) {
error = rule.message || `${field} is invalid`;
}
if (error) {
newErrors[field] = error;
isValid = false;
break;
}
}
});
setErrors(newErrors);
return isValid;
}, [values]);
/**
* 重置表单
*/
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setSubmitting(false);
}, [initialValues]);
/**
* 提交表单
*/
const handleSubmit = useCallback(async (onSubmit, validationRules) => {
if (validationRules && !validate(validationRules)) {
return false;
}
setSubmitting(true);
try {
await onSubmit(values);
return true;
} catch (error) {
console.error('Form submit error:', error);
return false;
} finally {
setSubmitting(false);
}
}, [values, validate]);
return {
values,
errors,
touched,
submitting,
handleChange,
setFieldsValue,
handleBlur,
setFieldError,
setErrorsAll,
validate,
resetForm,
handleSubmit,
};
};
```
### 3.6 useDebounce Hook
```javascript
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
/**
* 防抖Hook
*/
export const useDebounce = (value, delay = 300) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
};
```
### 3.7 useLocalStorage Hook
```javascript
// hooks/useLocalStorage.js
import { useState, useEffect, useCallback } from 'react';
/**
* LocalStorage Hook
*/
export const 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 reading localStorage key "${key}":`, 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 setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
};
```
## 4. 状态管理最佳实践
### 4.1 状态分类
```javascript
// 1. 本地状态 - 使用useState
const [value, setValue] = useState('');
// 2. 上下文状态 - 使用Context
const { user, login, logout } = useAuth();
// 3. URL状态 - 使用useSearchParams
const [searchParams, setSearchParams] = useSearchParams();
// 4. 表单状态 - 使用Form Hook
const [form] = Form.useForm();
// 5. 列表状态 - 使用自定义Hook
const { list, loading, pagination } = useList(projectAPI.getList);
```
### 4.2 状态提升
**错误示例:**
```jsx
// 父组件
const Parent = () => {
const [value, setValue] = useState('');
return <Child value={value} onChange={setValue} />;
};
// 子组件
const Child = ({ value, onChange }) => {
return <Input value={value} onChange={(e) => onChange(e.target.value)} />;
};
```
**正确示例:**
```jsx
// 状态留在子组件
const Child = () => {
const [value, setValue] = useState('');
return <Input value={value} onChange={(e) => setValue(e.target.value)} />;
};
```
### 4.3 避免不必要的重渲染
```jsx
// 使用React.memo
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* 渲染逻辑 */}</div>;
});
// 使用useMemo缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(data);
}, [data]);
// 使用useCallback缓存函数
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
```
## 5. 状态持久化
### 5.1 本地存储策略
```javascript
// 需要持久化的状态
const persistentState = {
user: 'localStorage', // 用户信息
token: 'localStorage', // 认证令牌
theme: 'localStorage', // 主题设置
language: 'localStorage', // 语言设置
};
// 不需要持久化的状态
const transientState = {
loading: false, // 加载状态
error: null, // 错误信息
modalVisible: false, // 弹窗状态
formData: {}, // 表单数据
};
```
### 5.2 状态恢复
```javascript
// 在应用启动时恢复状态
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
}
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
}, []);
```
## 6. 调试工具
### 6.1 React DevTools
```javascript
// 安装React DevTools浏览器扩展
// 在开发环境中,可以使用React DevTools查看组件树和状态
```
### 6.2 自定义状态日志
```javascript
// 开发环境下的状态日志
if (import.meta.env.DEV) {
useEffect(() => {
console.log('Auth state changed:', { user, token, authState });
}, [user, token, authState]);
}
```
### 6.3 状态变化监听
```javascript
// 监听特定状态的变化
useEffect(() => {
if (user) {
console.log('User logged in:', user);
} else {
console.log('User logged out');
}
}, [user]);
```
## 7. 性能优化
### 7.1 减少Context更新
```javascript
// 错误示例:频繁更新Context
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [timestamp, setTimestamp] = useState(Date.now());
// 这个timestamp变化会导致所有使用AuthContext的组件重渲染
useEffect(() => {
setInterval(() => {
setTimestamp(Date.now());
}, 1000);
}, []);
return <AuthContext.Provider value={{ user, timestamp }}>{children}</AuthContext.Provider>;
};
// 正确示例:分离频繁更新的状态
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [timestamp, setTimestamp] = useState(Date.now());
useEffect(() => {
setInterval(() => {
setTimestamp(Date.now());
}, 1000);
}, []);
return (
<>
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
<TimestampContext.Provider value={{ timestamp}}>{/* 不渲染 */}</TimestampContext.Provider>
</>
);
};
```
### 7.2 使用useReducer
```javascript
// 对于复杂的状态逻辑,使用useReducer
const initialState = {
user: null,
token: null,
loading: true,
error: null,
};
function authReducer(state, action) {
switch (action.type) {
case 'LOGIN_SUCCESS':
return {
...state,
user: action.payload.user,
token: action.payload.token,
loading: false,
};
case 'LOGOUT':
return {
...state,
user: null,
token: null,
};
default:
return state;
}
}
const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
};
```
## 8. 未来扩展
### 8.1 迁移到Redux
如果项目变得复杂,可以考虑迁移到Redux Toolkit
```javascript
// slices/authSlice.js
import { createSlice } from '@reduxjs/toolkit';
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: null,
loading: true,
},
reducers: {
setCredentials: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.loading = false;
},
logout: (state) => {
state.user = null;
state.token = null;
},
},
});
export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;
```
### 8.2 迁移到Zustand
Zustand是一个更轻量级的替代方案:
```javascript
// store/authStore.js
import { create } from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
token: null,
loading: true,
login: (user, token) => set({ user, token, loading: false }),
logout: () => set({ user: null, token: null }),
}));
```
---
**文档维护**: 前端程序员
**文档类型**: 状态管理设计文档
**最后更新**: 2026-01-25