refactor: 架构重构 - 解耦过度耦合模块
主要改动: 1. 创建乐观更新工具函数 (optimisticUpdate.ts) - 消除 userStore.ts 中的重复代码 2. 拆分 useResponsive.ts (485行 -> 12个专注模块) - useBreakpoint: 断点检测 - useOrientation: 方向检测 - usePlatform: 平台检测 - useScreenSize: 屏幕尺寸 - useResponsiveValue: 响应式值 - useResponsiveStyle: 响应式样式 - useMediaQuery: 媒体查询 - useColumnCount: 列数计算 - useResponsiveSpacing: 响应式间距 3. 整理数据层 (Repository 层) - ApiDataSource: API数据源 - LocalDataSource: 本地数据源 - CacheDataSource: 缓存数据源 - MessageRepository: 消息仓库 4. 重构 messageManager.ts (2194行 -> 4个模块) - MessageStateManager: 状态管理 - WebSocketMessageHandler: WebSocket处理 - MessageSyncService: 消息同步 - ReadReceiptManager: 已读管理 5. 导航解耦 (MainNavigator.tsx: 1118行 -> 100行) - 创建 NavigationService 解耦层 - 拆分多个 Navigator 组件 架构改进: - 单一职责原则: 每个模块职责明确 - 依赖倒置: 通过接口解耦 - 代码复用: 工具函数可被多处使用 - 可测试性: 各模块可独立测试
This commit is contained in:
287
src/utils/__tests__/optimisticUpdate.test.ts
Normal file
287
src/utils/__tests__/optimisticUpdate.test.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 乐观更新工具函数测试
|
||||
*/
|
||||
|
||||
import { optimisticUpdate, simpleOptimisticUpdate, createOptimisticUpdaterFactory } from '../optimisticUpdate';
|
||||
|
||||
// 模拟数据
|
||||
interface TestState {
|
||||
posts: Array<{ id: string; likes: number; isLiked: boolean }>;
|
||||
users: Array<{ id: string; isFollowing: boolean }>;
|
||||
}
|
||||
|
||||
const initialState: TestState = {
|
||||
posts: [
|
||||
{ id: '1', likes: 10, isLiked: false },
|
||||
{ id: '2', likes: 5, isLiked: true },
|
||||
],
|
||||
users: [
|
||||
{ id: 'user1', isFollowing: false },
|
||||
{ id: 'user2', isFollowing: true },
|
||||
],
|
||||
};
|
||||
|
||||
describe('optimisticUpdate', () => {
|
||||
let currentState: TestState;
|
||||
|
||||
beforeEach(() => {
|
||||
currentState = JSON.parse(JSON.stringify(initialState));
|
||||
});
|
||||
|
||||
describe('成功场景', () => {
|
||||
it('应该执行乐观更新并在API成功后保持更新', async () => {
|
||||
const mockApiCall = jest.fn().mockResolvedValue({ success: true, likes: 11 });
|
||||
|
||||
const result = await optimisticUpdate<TestState, { success: boolean; likes: number }>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
}),
|
||||
apiCall: mockApiCall,
|
||||
onSuccess: (result, current) => ({
|
||||
...current,
|
||||
posts: current.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: result.likes } : p
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
// 验证API被调用
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
// 验证返回结果
|
||||
expect(result).toEqual({ success: true, likes: 11 });
|
||||
});
|
||||
|
||||
it('应该在乐观更新后立即改变状态', async () => {
|
||||
const mockApiCall = jest.fn().mockImplementation(() => {
|
||||
// 在API调用期间验证状态已被乐观更新
|
||||
expect(currentState.posts[0].isLiked).toBe(true);
|
||||
expect(currentState.posts[0].likes).toBe(11);
|
||||
return Promise.resolve({ success: true });
|
||||
});
|
||||
|
||||
await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
};
|
||||
currentState = newState; // 模拟状态更新
|
||||
return newState;
|
||||
},
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('失败回滚场景', () => {
|
||||
it('应该在API失败时回滚到原始状态', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
const onError = jest.fn((error, original) => original);
|
||||
|
||||
const result = await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p
|
||||
),
|
||||
}),
|
||||
apiCall: mockApiCall,
|
||||
onError,
|
||||
errorMessage: '点赞失败',
|
||||
});
|
||||
|
||||
// 验证API被调用
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
// 验证onError被调用
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
initialState
|
||||
);
|
||||
// 验证返回undefined(因为失败了)
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该在静默模式下不抛出错误', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// 不应该抛出错误
|
||||
await expect(
|
||||
optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该在非静默模式下抛出错误', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// 应该抛出错误
|
||||
await expect(
|
||||
optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
silent: false,
|
||||
})
|
||||
).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该记录错误日志', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('API Error'));
|
||||
|
||||
await optimisticUpdate<TestState, any>({
|
||||
getState: () => currentState,
|
||||
optimisticUpdate: (state) => state,
|
||||
apiCall: mockApiCall,
|
||||
errorMessage: '自定义错误消息',
|
||||
silent: true,
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('自定义错误消息:', expect.any(Error));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleOptimisticUpdate', () => {
|
||||
let currentState: TestState;
|
||||
let setStateMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
currentState = JSON.parse(JSON.stringify(initialState));
|
||||
setStateMock = jest.fn((newState) => {
|
||||
currentState = newState;
|
||||
});
|
||||
});
|
||||
|
||||
it('应该成功执行乐观更新', async () => {
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await simpleOptimisticUpdate<TestState>({
|
||||
getState: () => currentState,
|
||||
setState: setStateMock,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1 } : p
|
||||
),
|
||||
}),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
expect(setStateMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('应该在失败时回滚状态', async () => {
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Error'));
|
||||
const rollbackMock = jest.fn((original) => original);
|
||||
|
||||
await simpleOptimisticUpdate<TestState>({
|
||||
getState: () => currentState,
|
||||
setState: setStateMock,
|
||||
optimisticUpdate: (state) => ({
|
||||
...state,
|
||||
posts: state.posts.map((p) =>
|
||||
p.id === '1' ? { ...p, likes: p.likes + 1 } : p
|
||||
),
|
||||
}),
|
||||
rollbackState: rollbackMock,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
// 验证乐观更新和回滚都被调用
|
||||
expect(setStateMock).toHaveBeenCalledTimes(2);
|
||||
expect(rollbackMock).toHaveBeenCalledWith(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOptimisticUpdaterFactory', () => {
|
||||
interface StoreState {
|
||||
posts: Array<{ id: string; likes: number }>;
|
||||
count: number;
|
||||
}
|
||||
|
||||
let storeState: StoreState;
|
||||
let setMock: jest.Mock;
|
||||
let getMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
storeState = {
|
||||
posts: [{ id: '1', likes: 10 }],
|
||||
count: 0,
|
||||
};
|
||||
setMock = jest.fn((fn) => {
|
||||
storeState = fn(storeState);
|
||||
});
|
||||
getMock = jest.fn(() => storeState);
|
||||
});
|
||||
|
||||
it('应该创建乐观更新器并正常工作', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(mockApiCall).toHaveBeenCalledTimes(1);
|
||||
expect(setMock).toHaveBeenCalledTimes(1);
|
||||
expect(storeState.posts[0].likes).toBe(11);
|
||||
});
|
||||
|
||||
it('应该在API失败时回滚状态', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockRejectedValue(new Error('Error'));
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
// 乐观更新 + 回滚 = 2次set调用
|
||||
expect(setMock).toHaveBeenCalledTimes(2);
|
||||
// 最终状态应该和初始状态一样
|
||||
expect(storeState.posts[0].likes).toBe(10);
|
||||
});
|
||||
|
||||
it('应该处理不同的state key', async () => {
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<StoreState>(setMock, getMock);
|
||||
const mockApiCall = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await optimisticUpdater({
|
||||
key: 'count',
|
||||
optimisticUpdate: (count) => count + 1,
|
||||
rollbackState: (original) => original,
|
||||
apiCall: mockApiCall,
|
||||
});
|
||||
|
||||
expect(storeState.count).toBe(1);
|
||||
});
|
||||
});
|
||||
201
src/utils/optimisticUpdate.ts
Normal file
201
src/utils/optimisticUpdate.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 乐观更新工具函数
|
||||
* 用于统一管理乐观更新逻辑,减少重复代码
|
||||
*/
|
||||
|
||||
export interface OptimisticUpdateOptions<T, R> {
|
||||
/** 获取当前状态 */
|
||||
getState: () => T;
|
||||
/** 执行乐观更新 */
|
||||
optimisticUpdate: (current: T) => T;
|
||||
/** API调用函数 */
|
||||
apiCall: () => Promise<R>;
|
||||
/** API成功时的回调,返回新的状态 */
|
||||
onSuccess?: (result: R, current: T) => T;
|
||||
/** API失败时的回滚回调 */
|
||||
onError?: (error: any, original: T) => T;
|
||||
/** 错误消息 */
|
||||
errorMessage?: string;
|
||||
/** 是否静默处理错误(不抛出) */
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行乐观更新
|
||||
*
|
||||
* 流程:
|
||||
* 1. 保存原始状态
|
||||
* 2. 执行乐观更新
|
||||
* 3. 调用API
|
||||
* 4. 成功时使用服务器数据更新状态
|
||||
* 5. 失败时回滚到原始状态
|
||||
*
|
||||
* @template T 状态类型
|
||||
* @template R API返回类型
|
||||
* @param options 配置选项
|
||||
* @returns Promise<R> API返回结果
|
||||
*/
|
||||
export async function optimisticUpdate<T, R>(
|
||||
options: OptimisticUpdateOptions<T, R>
|
||||
): Promise<R | undefined> {
|
||||
const {
|
||||
getState,
|
||||
optimisticUpdate,
|
||||
apiCall,
|
||||
onSuccess,
|
||||
onError,
|
||||
errorMessage = '操作失败',
|
||||
silent = true,
|
||||
} = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalState = getState();
|
||||
|
||||
// 执行乐观更新
|
||||
const optimisticState = optimisticUpdate(originalState);
|
||||
|
||||
try {
|
||||
// 调用API
|
||||
const result = await apiCall();
|
||||
|
||||
// API成功,使用服务器数据更新状态(如果提供了onSuccess)
|
||||
if (onSuccess) {
|
||||
onSuccess(result, optimisticState);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error);
|
||||
|
||||
// API失败,回滚状态
|
||||
if (onError) {
|
||||
onError(error, originalState);
|
||||
}
|
||||
|
||||
// 如果不是静默模式,抛出错误
|
||||
if (!silent) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化版的乐观更新(适用于不需要服务器返回数据的场景)
|
||||
*
|
||||
* @template T 状态类型
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export async function simpleOptimisticUpdate<T>(
|
||||
options: Omit<OptimisticUpdateOptions<T, void>, 'onSuccess' | 'onError'> & {
|
||||
setState: (state: T) => void;
|
||||
rollbackState: (original: T) => T;
|
||||
}
|
||||
): Promise<void> {
|
||||
const {
|
||||
getState,
|
||||
setState,
|
||||
optimisticUpdate,
|
||||
rollbackState,
|
||||
apiCall,
|
||||
errorMessage = '操作失败',
|
||||
silent = true,
|
||||
} = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalState = getState();
|
||||
|
||||
// 执行乐观更新
|
||||
setState(optimisticUpdate(originalState));
|
||||
|
||||
try {
|
||||
// 调用API
|
||||
await apiCall();
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error);
|
||||
|
||||
// API失败,回滚状态
|
||||
setState(rollbackState(originalState));
|
||||
|
||||
// 如果不是静默模式,抛出错误
|
||||
if (!silent) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建乐观更新器(用于Zustand store)
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* const createOptimisticUpdater = createOptimisticUpdaterFactory(set, get);
|
||||
*
|
||||
* const likePost = async (postId: string) => {
|
||||
* await createOptimisticUpdater({
|
||||
* getCurrentState: () => get().posts,
|
||||
* setState: (posts) => set({ posts }),
|
||||
* optimisticUpdate: (posts) => posts.map(...),
|
||||
* rollbackState: (originalPosts) => originalPosts,
|
||||
* apiCall: () => postService.likePost(postId),
|
||||
* errorMessage: '点赞失败',
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export function createOptimisticUpdaterFactory<T extends Record<string, any>>(
|
||||
set: (fn: (state: T) => T) => void,
|
||||
get: () => T
|
||||
) {
|
||||
return function optimisticUpdater<K extends keyof T>(
|
||||
options: {
|
||||
key: K;
|
||||
optimisticUpdate: (current: T[K]) => T[K];
|
||||
rollbackState: (original: T[K]) => T[K];
|
||||
apiCall: () => Promise<any>;
|
||||
errorMessage?: string;
|
||||
silent?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
const {
|
||||
key,
|
||||
optimisticUpdate,
|
||||
rollbackState,
|
||||
apiCall,
|
||||
errorMessage = '操作失败',
|
||||
silent = true,
|
||||
} = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalState = get()[key];
|
||||
|
||||
// 执行乐观更新
|
||||
set((state) => ({
|
||||
...state,
|
||||
[key]: optimisticUpdate(state[key]),
|
||||
}));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
apiCall()
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`${errorMessage}:`, error);
|
||||
|
||||
// 回滚状态
|
||||
set((state) => ({
|
||||
...state,
|
||||
[key]: rollbackState(originalState),
|
||||
}));
|
||||
|
||||
if (silent) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ViewStyle } from 'react-native';
|
||||
import { BREAKPOINTS } from '../hooks/useResponsive';
|
||||
import { BREAKPOINTS } from '../presentation/hooks/responsive';
|
||||
|
||||
/**
|
||||
* 断点键名
|
||||
|
||||
Reference in New Issue
Block a user