refactor: 优化 RouterProvider 组件的数据加载逻辑

- 使用批量获取方法 `getMany` 替代多个单独的 `get` 调用,简化数据加载过程。
- 更新 `loadInitialData` 函数,确保从存储中获取的默认值更为一致。
- 修改测试用例以验证批量获取的功能,确保组件在不同情况下的正确性。
This commit is contained in:
2026-06-30 22:56:31 +08:00
parent 27e03ae038
commit 17a4dbd8a7
10 changed files with 138 additions and 58 deletions
+23 -19
View File
@@ -110,13 +110,19 @@ export function RouterProvider({
const loadInitialData = useCallback(async () => {
try {
const savedRoute = await storageUtil.get(syncKey, defaultRoute);
const savedVisiblePages = await storageUtil.get(
const stored = await storageUtil.getMany([
syncKey,
visiblePagesKey,
getDefaultVisibleFeatureKeys(),
);
const savedPageOrder = await storageUtil.get(pageOrderKey, getDefaultPageOrder());
const savedRecentTools = await storageUtil.get('app/recentlyUsedTools', []);
pageOrderKey,
'app/recentlyUsedTools',
'contextMenu/pendingData',
]);
const savedRoute = (stored[syncKey] ?? defaultRoute) as PageType;
const savedVisiblePages = (stored[visiblePagesKey] ??
getDefaultVisibleFeatureKeys()) as PageType[];
const savedPageOrder = (stored[pageOrderKey] ?? getDefaultPageOrder()) as PageType[];
const savedRecentTools = (stored['app/recentlyUsedTools'] ?? []) as PageType[];
if (isValidPage(savedRoute) && syncRoute && !hasUserNavigatedRef.current) {
setCurrentPage(savedRoute);
@@ -131,8 +137,11 @@ export function RouterProvider({
setRecentlyUsedTools(savedRecentTools);
}
canPersistRef.current = true;
return stored['contextMenu/pendingData'];
} catch (error) {
console.error('[Router Init Error] Core data fetch failed:', error);
return undefined;
} finally {
setIsLoaded(true);
}
@@ -143,7 +152,7 @@ export function RouterProvider({
// eslint-disable-next-line react-hooks/set-state-in-effect -- Valid async data loading pattern on mount
loadInitialData()
.then(() => {
.then((pendingData) => {
if (cancelled) return;
if (typeof window !== 'undefined') {
@@ -165,18 +174,13 @@ export function RouterProvider({
}
}
storageUtil
.get('contextMenu/pendingData')
.then((pendingData) => {
if (
pendingData &&
isValidPage(pendingData.featureKey) &&
Date.now() - pendingData.timestamp < CONTEXT_MENU_DATA_EXPIRY_MS
) {
setCurrentPage(pendingData.featureKey as PageType);
}
})
.catch(console.error);
if (
pendingData &&
isValidPage(pendingData.featureKey) &&
Date.now() - pendingData.timestamp < CONTEXT_MENU_DATA_EXPIRY_MS
) {
setCurrentPage(pendingData.featureKey as PageType);
}
})
.catch(console.error);
+5
View File
@@ -13,6 +13,7 @@ import { storageUtil } from '@/utils/chromeStorage';
import {
applyResolvedThemeClass,
getThemeSyncSnapshot,
hasThemeSyncSnapshot,
isValidThemeMode,
persistThemeModeSnapshot,
resolveThemeMode,
@@ -68,6 +69,10 @@ export function ThemeModeProvider({ children }: ThemeModeProviderProps) {
);
useEffect(() => {
if (hasThemeSyncSnapshot()) {
return;
}
let cancelled = false;
storageUtil
+43 -37
View File
@@ -7,10 +7,24 @@ import { storageUtil } from '@/utils/chromeStorage';
vi.mock('@/utils/chromeStorage', () => ({
storageUtil: {
get: vi.fn(),
getMany: vi.fn(),
set: vi.fn(() => Promise.resolve()),
},
}));
/** 模拟批量 storage 读取,未指定的键由 Router 侧使用默认值 */
function mockStorageBatch(map: Record<string, unknown> = {}) {
(storageUtil.getMany as ReturnType<typeof vi.fn>).mockImplementation((keys: string[]) => {
const result: Record<string, unknown> = {};
for (const key of keys) {
if (key in map) {
result[key] = map[key];
}
}
return Promise.resolve(result);
});
}
const TestComponent = () => {
const { currentPage, navigateTo, visiblePages, pageOrder } = useRouter();
return (
@@ -46,9 +60,7 @@ describe('RouterProvider', () => {
});
it('应该使用默认值初始化路由', async () => {
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
Promise.resolve(defaultValue),
);
mockStorageBatch();
render(
<RouterProvider defaultRoute="dashboard">
@@ -59,13 +71,18 @@ describe('RouterProvider', () => {
await waitFor(() => {
expect(screen.getByTestId('current-page')).toHaveTextContent('dashboard');
});
expect(storageUtil.getMany).toHaveBeenCalledWith([
'app/currentRoute',
'app/visiblePages',
'app/pageOrder',
'app/recentlyUsedTools',
'contextMenu/pendingData',
]);
});
it('应该从指定的 syncKey 加载路由', async () => {
(storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => {
if (key === 'app/sidepanelRoute') return Promise.resolve('timestamp');
return Promise.resolve(defaultValue);
});
mockStorageBatch({ 'app/sidepanelRoute': 'timestamp' });
render(
<RouterProvider syncKey="app/sidepanelRoute">
@@ -79,9 +96,7 @@ describe('RouterProvider', () => {
});
it('导航时应该更新指定的 syncKey', async () => {
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
Promise.resolve(defaultValue),
);
mockStorageBatch();
render(
<RouterProvider syncKey="app/popupRoute">
@@ -104,10 +119,7 @@ describe('RouterProvider', () => {
});
it('应该支持独立的标签页路由同步', async () => {
(storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => {
if (key === 'app/tabRoute') return Promise.resolve('qrCode');
return Promise.resolve(defaultValue);
});
mockStorageBatch({ 'app/tabRoute': 'qrCode' });
render(
<RouterProvider syncKey="app/tabRoute">
@@ -121,11 +133,9 @@ describe('RouterProvider', () => {
});
it('应该独立支持 visiblePagesKey 和 pageOrderKey,并合并缺失的新功能', async () => {
(storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => {
if (key === 'app/sidepanelVisiblePages')
return Promise.resolve(['timestamp', 'storageCleaner']);
if (key === 'app/sidepanelPageOrder') return Promise.resolve(['storageCleaner', 'timestamp']);
return Promise.resolve(defaultValue);
mockStorageBatch({
'app/sidepanelVisiblePages': ['timestamp', 'storageCleaner'],
'app/sidepanelPageOrder': ['storageCleaner', 'timestamp'],
});
render(
@@ -167,10 +177,9 @@ describe('RouterProvider', () => {
'jsonTools',
];
(storageUtil.get as any).mockImplementation((key: string, defaultValue: any) => {
if (key === 'app/popupVisiblePages') return Promise.resolve(oldVisiblePages);
if (key === 'app/popupPageOrder') return Promise.resolve(oldPageOrder);
return Promise.resolve(defaultValue);
mockStorageBatch({
'app/popupVisiblePages': oldVisiblePages,
'app/popupPageOrder': oldPageOrder,
});
render(
@@ -209,9 +218,7 @@ describe('RouterProvider', () => {
localStorage.setItem('snapshot/app/visiblePages', JSON.stringify(oldVisiblePages));
localStorage.setItem('snapshot/app/pageOrder', JSON.stringify(oldPageOrder));
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: any) =>
Promise.resolve(defaultValue),
);
mockStorageBatch();
render(
<RouterProvider>
@@ -226,9 +233,7 @@ describe('RouterProvider', () => {
});
it('storage.onChanged 同步 visiblePages 时应合并缺失的新功能', async () => {
(storageUtil.get as any).mockImplementation((_key: string, defaultValue: unknown) =>
Promise.resolve(defaultValue),
);
mockStorageBatch();
render(
<RouterProvider visiblePagesKey="app/popupVisiblePages" pageOrderKey="app/popupPageOrder">
@@ -299,9 +304,9 @@ describe('RouterProvider', () => {
resolveGet = resolve;
});
(storageUtil.get as any).mockImplementation(async (key: string, defaultValue: unknown) => {
(storageUtil.getMany as ReturnType<typeof vi.fn>).mockImplementation(async () => {
await getBlocked;
return storage.get(key) ?? defaultValue;
return Object.fromEntries(storage.entries());
});
(storageUtil.set as any).mockImplementation(async (key: string, value: unknown) => {
storage.set(key, value);
@@ -324,7 +329,9 @@ describe('RouterProvider', () => {
});
it('初始化失败时仍应解除加载状态以便渲染页面', async () => {
(storageUtil.get as any).mockRejectedValue(new Error('Storage unavailable'));
(storageUtil.getMany as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Storage unavailable'),
);
render(
<RouterProvider>
@@ -344,10 +351,9 @@ describe('RouterProvider', () => {
resolveGet = resolve;
});
(storageUtil.get as any).mockImplementation(async (key: string, defaultValue: unknown) => {
(storageUtil.getMany as ReturnType<typeof vi.fn>).mockImplementation(async () => {
await getBlocked;
if (key === 'app/currentRoute') return 'dashboard';
return defaultValue;
return { 'app/currentRoute': 'dashboard' };
});
render(
@@ -374,7 +380,7 @@ describe('RouterProvider', () => {
resolveStorage = resolve;
});
(storageUtil.get as any).mockImplementation(() => storagePromise);
(storageUtil.getMany as ReturnType<typeof vi.fn>).mockImplementation(() => storagePromise);
const { unmount } = render(
<RouterProvider>
@@ -383,7 +389,7 @@ describe('RouterProvider', () => {
);
unmount();
resolveStorage!('dashboard');
resolveStorage!({});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
@@ -46,6 +46,7 @@ describe('ThemeModeProvider', () => {
expect(screen.getByTestId('mode')).toHaveTextContent('dark');
expect(screen.getByTestId('resolved-mode')).toHaveTextContent('dark');
expect(document.documentElement.classList.contains('dark')).toBe(true);
expect(storageUtil.get).not.toHaveBeenCalled();
});
it('异步恢复 storage 后应写回 snapshot', async () => {