From f9fbc796129773bff9e8132f5c83cc52028682a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 29 May 2026 21:59:54 +0800 Subject: [PATCH] refactor(storageCleaner): simplify architecture and improve type semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly - Rename IndexedDB label to '站点存储' for accuracy - Introduce StorageSizeInfo type to distinguish bytes vs count - Rename totalSize to totalBytes for clarity - Merge runCleanScript into runScript to reduce duplication - Rename CleaningResult.success to overallSuccess to avoid confusion - Remove unused domain state and setDomain call Co-Authored-By: Claude Opus 4.8 --- public/_locales/zh/messages.json | 2 +- src/entrypoints/background.ts | 18 -- src/pages/StorageCleaner/CleaningResult.tsx | 2 +- src/pages/StorageCleaner/OptionItem.tsx | 15 +- .../StorageCleaner/StorageOptionsGrid.tsx | 22 +-- src/pages/StorageCleaner/useStorageCleaner.ts | 45 +++-- src/types/storage.d.ts | 2 +- src/utils/__tests__/messages.test.ts | 24 +-- src/utils/messages.ts | 2 - src/utils/storageCleaner.ts | 165 ++++++++++-------- vitest.setup.ts | 3 +- 11 files changed, 149 insertions(+), 151 deletions(-) diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json index 720c853..fc9491f 100644 --- a/public/_locales/zh/messages.json +++ b/public/_locales/zh/messages.json @@ -697,7 +697,7 @@ "description": "Translation key: storageCleaner_options_sessionStorage" }, "storageCleaner_options_indexedDB": { - "message": "IndexedDB", + "message": "站点存储", "description": "Translation key: storageCleaner_options_indexedDB" }, "storageCleaner_options_cookies": { diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index 2a7ff00..d1b177b 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -83,22 +83,4 @@ export default defineBackground(() => { return { success: false, message: errorMsg }; } }); - - onMessage(MessageAction.RELOAD_TAB, async (message) => { - const { tabId, delay = 0 } = message.data; - - const executeReload = () => { - browser.tabs.reload(tabId).catch((err) => { - console.error('Failed to execute tab reload operation:', err); - }); - }; - - if (delay > 0) { - setTimeout(executeReload, delay); - } else { - executeReload(); - } - - return { success: true }; - }); }); diff --git a/src/pages/StorageCleaner/CleaningResult.tsx b/src/pages/StorageCleaner/CleaningResult.tsx index d546d09..7025838 100644 --- a/src/pages/StorageCleaner/CleaningResult.tsx +++ b/src/pages/StorageCleaner/CleaningResult.tsx @@ -14,7 +14,7 @@ export default function CleaningResult({ result, className, ...props }: Cleaning if (!result) return null; - const isSuccess = result.success; + const isSuccess = result.overallSuccess; return (
diff --git a/src/pages/StorageCleaner/OptionItem.tsx b/src/pages/StorageCleaner/OptionItem.tsx index f88ad8e..a6c96ee 100644 --- a/src/pages/StorageCleaner/OptionItem.tsx +++ b/src/pages/StorageCleaner/OptionItem.tsx @@ -2,28 +2,29 @@ import React from 'react'; import { formatBytes } from '@/utils/format'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; -// 引入官方的 Checkbox 原子组件 +import type { StorageSizeInfo } from './useStorageCleaner'; import { Checkbox } from '@/components/ui/checkbox'; interface OptionItemProps extends React.HTMLAttributes { labelKey: string; checked: boolean; - size?: number; - isCount?: boolean; + sizeInfo?: StorageSizeInfo; onChange: () => void; } export default function OptionItem({ labelKey, checked, - size, - isCount = false, + sizeInfo, onChange, className, ...props }: OptionItemProps) { const { t } = useI18n('storageCleaner'); + const sizeValue = sizeInfo?.value; + const isCount = sizeInfo?.displayType === 'count'; + return (
- {size !== undefined && size > 0 ? ( + {sizeValue !== undefined && sizeValue > 0 ? ( - {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatBytes(size)} + {isCount ? `${sizeValue} ${t('storageCleaner:countUnit')}` : formatBytes(sizeValue)} ) : ( diff --git a/src/pages/StorageCleaner/StorageOptionsGrid.tsx b/src/pages/StorageCleaner/StorageOptionsGrid.tsx index 352abaf..f19e36a 100644 --- a/src/pages/StorageCleaner/StorageOptionsGrid.tsx +++ b/src/pages/StorageCleaner/StorageOptionsGrid.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { StorageCleanerOptions } from '@/types/storage'; +import type { StorageSizeInfo } from './useStorageCleaner'; import OptionItem from './OptionItem'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; @@ -8,7 +9,7 @@ import { Label } from '@/components/ui/label'; interface StorageOptionsGridProps extends React.HTMLAttributes { options: StorageCleanerOptions; - sizes: Record; + sizes: Record; allSelected: boolean; someSelected: boolean; onOptionChange: (key: keyof StorageCleanerOptions) => void; @@ -27,13 +28,13 @@ export default function StorageOptionsGrid({ }: StorageOptionsGridProps) { const { t } = useI18n('storageCleaner'); - const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [ - { key: 'localStorage' }, - { key: 'sessionStorage' }, - { key: 'indexedDB' }, - { key: 'cookies' }, - { key: 'cacheStorage', isCount: true }, - { key: 'serviceWorkers', isCount: true }, + const optionKeys: (keyof StorageCleanerOptions)[] = [ + 'localStorage', + 'sessionStorage', + 'indexedDB', + 'cookies', + 'cacheStorage', + 'serviceWorkers', ]; const handleToggleAll = () => { @@ -44,13 +45,12 @@ export default function StorageOptionsGrid({
- {optionKeys.map(({ key, isCount }) => ( + {optionKeys.map((key) => ( onOptionChange(key)} /> ))} diff --git a/src/pages/StorageCleaner/useStorageCleaner.ts b/src/pages/StorageCleaner/useStorageCleaner.ts index 5910df4..9b46579 100644 --- a/src/pages/StorageCleaner/useStorageCleaner.ts +++ b/src/pages/StorageCleaner/useStorageCleaner.ts @@ -16,7 +16,6 @@ import { getSessionStorageSize, isRestrictedUrl, } from '@/utils/storageCleaner'; -import { MessageAction, sendMessage } from '@/utils/messages'; import { useI18n } from '@/utils/chromeI18n'; import { toast } from 'sonner'; @@ -34,18 +33,22 @@ const DEFAULT_PREFERENCES: StorageCleanerPreferences = { selectedTypes: DEFAULT_OPTIONS, }; +export interface StorageSizeInfo { + value: number; + displayType: 'bytes' | 'count'; +} + export interface UseStorageCleanerReturn { - domain: string; error: string; isInitializing: boolean; options: StorageCleanerOptions; - sizes: Record; + sizes: Record; reloadAfterClean: boolean; loading: boolean; result: CleaningResult | null; showConfirm: boolean; setShowConfirm: (show: boolean) => void; - totalSize: number; + totalBytes: number; allSelected: boolean; someSelected: boolean; @@ -57,11 +60,10 @@ export interface UseStorageCleanerReturn { export function useStorageCleaner(): UseStorageCleanerReturn { const { t } = useI18n(['storageCleaner', 'common']); - const [domain, setDomain] = useState(''); const [error, setError] = useState(''); const [isInitializing, setIsInitializing] = useState(true); const [options, setOptions] = useState(DEFAULT_OPTIONS); - const [sizes, setSizes] = useState>({}); + const [sizes, setSizes] = useState>({}); const [reloadAfterClean, setReloadAfterClean] = useState(true); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); @@ -102,7 +104,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn { setError(''); const url = tab.url; const tabId = tab.id!; - setDomain(new URL(url).hostname); const [savedPrefs, cSize, lsSize, ssSize, idbSize, cacheCount, swCount] = await Promise.all([ storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES), @@ -122,12 +123,12 @@ export function useStorageCleaner(): UseStorageCleanerReturn { } setSizes({ - cookies: cSize, - localStorage: lsSize, - sessionStorage: ssSize, - indexedDB: idbSize, - cacheStorage: cacheCount, - serviceWorkers: swCount, + cookies: { value: cSize, displayType: 'bytes' }, + localStorage: { value: lsSize, displayType: 'bytes' }, + sessionStorage: { value: ssSize, displayType: 'bytes' }, + indexedDB: { value: idbSize, displayType: 'bytes' }, + cacheStorage: { value: cacheCount, displayType: 'count' }, + serviceWorkers: { value: swCount, displayType: 'count' }, }); } finally { if (currentRequestId === requestIdRef.current) { @@ -217,9 +218,9 @@ export function useStorageCleaner(): UseStorageCleanerReturn { const cleaningResult = await clearStorage(tab.id, tab.url, options); setResult(cleaningResult); - if (reloadAfterClean && cleaningResult.success) { + if (reloadAfterClean && cleaningResult.overallSuccess) { toast.success(t('storageCleaner:cleanSuccessReload')); - await sendMessage(MessageAction.RELOAD_TAB, { tabId: tab.id, delay: 1000 }); + await chrome.tabs.reload(tab.id); } else { await loadInfo(); } @@ -231,13 +232,10 @@ export function useStorageCleaner(): UseStorageCleanerReturn { } }, [options, reloadAfterClean, loadInfo, t]); - const totalSize = useMemo(() => { - return ( - (sizes.cookies || 0) + - (sizes.localStorage || 0) + - (sizes.sessionStorage || 0) + - (sizes.indexedDB || 0) - ); + const totalBytes = useMemo(() => { + return Object.values(sizes).reduce((acc, s) => { + return s.displayType === 'bytes' ? acc + (s.value || 0) : acc; + }, 0); }, [sizes]); const selectionMetrics = useMemo(() => { @@ -248,7 +246,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn { }, [options]); return { - domain, error, isInitializing, options, @@ -258,7 +255,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn { result, showConfirm, setShowConfirm, - totalSize, + totalBytes, allSelected: selectionMetrics.all, someSelected: selectionMetrics.some, handleReloadAfterCleanChange, diff --git a/src/types/storage.d.ts b/src/types/storage.d.ts index 64fe134..f5ab210 100644 --- a/src/types/storage.d.ts +++ b/src/types/storage.d.ts @@ -189,7 +189,7 @@ export type StorageCleanResult = */ export interface CleaningResult { /** 整体操作是否成功 */ - success: boolean; + overallSuccess: boolean; /** 整体错误信息(如果有) */ error?: string; /** 各项清理的具体结果 */ diff --git a/src/utils/__tests__/messages.test.ts b/src/utils/__tests__/messages.test.ts index 7468edb..434361c 100644 --- a/src/utils/__tests__/messages.test.ts +++ b/src/utils/__tests__/messages.test.ts @@ -34,10 +34,14 @@ describe('messages', () => { const mockTab = { id: 123, url: 'https://example.com' }; (chrome.tabs.query as any).mockResolvedValue([mockTab]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual(mockResponse); - expect(mockSendMessage).toHaveBeenCalledWith(MessageAction.RELOAD_TAB, { tabId: 123 }, 123); + expect(mockSendMessage).toHaveBeenCalledWith( + MessageAction.RESTORE_RIGHT_CLICK, + undefined, + 123, + ); }); it('应该支持不带数据的消息发送', async () => { @@ -60,11 +64,11 @@ describe('messages', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); (chrome.tabs.query as any).mockResolvedValue([]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, message: '无法获取当前标签页' }); expect(consoleSpy).toHaveBeenCalledWith( - '[Messaging] 无法获取当前标签页,无法发送动作: reloadTab', + '[Messaging] 无法获取当前标签页,无法发送动作: restoreRightClick', ); consoleSpy.mockRestore(); }); @@ -73,7 +77,7 @@ describe('messages', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); (chrome.tabs.query as any).mockResolvedValue([{ url: 'https://example.com' }]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, message: '无法获取当前标签页' }); consoleSpy.mockRestore(); @@ -87,7 +91,7 @@ describe('messages', () => { const mockTab = { id: 123, url: 'https://example.com' }; (chrome.tabs.query as any).mockResolvedValue([mockTab]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, @@ -104,7 +108,7 @@ describe('messages', () => { const mockTab = { id: 123, url: 'https://example.com' }; (chrome.tabs.query as any).mockResolvedValue([mockTab]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, @@ -121,7 +125,7 @@ describe('messages', () => { const mockTab = { id: 123, url: 'https://example.com' }; (chrome.tabs.query as any).mockResolvedValue([mockTab]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, @@ -138,7 +142,7 @@ describe('messages', () => { const mockTab = { id: 123, url: 'https://example.com' }; (chrome.tabs.query as any).mockResolvedValue([mockTab]); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, @@ -151,7 +155,7 @@ describe('messages', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); (chrome.tabs.query as any).mockRejectedValue(new Error('Query failed')); - const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 }); + const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); expect(result).toEqual({ success: false, diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 476c4bd..b1f9bcb 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,7 +1,6 @@ import { defineExtensionMessaging } from '@webext-core/messaging'; export enum MessageAction { - RELOAD_TAB = 'reloadTab', SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged', CONTEXT_MENU_CLICKED = 'contextMenuClicked', RESTORE_RIGHT_CLICK = 'restoreRightClick', @@ -21,7 +20,6 @@ export interface ContextMenuClickedPayload { } export interface ProtocolMap { - [MessageAction.RELOAD_TAB](data: { tabId: number; delay?: number }): MessageResponse; [MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void; [MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void; [MessageAction.RESTORE_RIGHT_CLICK](data: undefined): MessageResponse & { restored: boolean }; diff --git a/src/utils/storageCleaner.ts b/src/utils/storageCleaner.ts index f40fea2..1238e07 100644 --- a/src/utils/storageCleaner.ts +++ b/src/utils/storageCleaner.ts @@ -215,98 +215,115 @@ export async function clearCookies(url: string): Promise { async function runCleanScript( tabId: number, func: () => { count: number } | Promise<{ count: number }>, + errorLabel: string, ): Promise { - try { - const [result] = await chrome.scripting.executeScript({ target: { tabId }, func }); - if (result?.result && typeof result.result === 'object' && 'count' in result.result) { - return { success: true, count: result.result.count }; - } - return { success: false, error: 'No result returned' }; - } catch (error) { - return { success: false, error: String(error) }; + const raw = await runScript(tabId, func, errorLabel, { count: 0 }); + if (raw && typeof raw === 'object' && 'count' in raw) { + return { success: true, count: raw.count }; } + return { success: false, error: 'No result returned' }; } export async function injectClearLocalStorage(tabId: number): Promise { - return runCleanScript(tabId, () => { - const count = localStorage.length; - localStorage.clear(); - return { count }; - }); + return runCleanScript( + tabId, + () => { + const count = localStorage.length; + localStorage.clear(); + return { count }; + }, + 'clear LocalStorage', + ); } export async function injectClearSessionStorage(tabId: number): Promise { - return runCleanScript(tabId, () => { - const count = sessionStorage.length; - sessionStorage.clear(); - return { count }; - }); + return runCleanScript( + tabId, + () => { + const count = sessionStorage.length; + sessionStorage.clear(); + return { count }; + }, + 'clear SessionStorage', + ); } export async function injectClearIndexedDB(tabId: number): Promise { - return runCleanScript(tabId, async () => { - if (typeof indexedDB.databases !== 'function') { - return { count: 0 }; - } - const databases = await indexedDB.databases(); - let count = 0; - for (const db of databases) { - if (!db.name) continue; - const dbName = db.name; - try { - await new Promise((resolve, reject) => { - const deleteReq = indexedDB.deleteDatabase(dbName); - const timeout = setTimeout(() => { - console.warn('IndexedDB delete timeout:', dbName); - resolve(); - }, 5000); - deleteReq.onblocked = () => { - console.warn('IndexedDB delete blocked:', dbName); - clearTimeout(timeout); - resolve(); - }; - deleteReq.onsuccess = () => { - clearTimeout(timeout); - resolve(); - }; - deleteReq.onerror = () => { - clearTimeout(timeout); - reject(new Error(`Failed to delete ${dbName}`)); - }; - }); - count++; - } catch (e) { - console.error('Delete DB error:', e); + return runCleanScript( + tabId, + async () => { + if (typeof indexedDB.databases !== 'function') { + return { count: 0 }; } - } - return { count }; - }); + const databases = await indexedDB.databases(); + let count = 0; + for (const db of databases) { + if (!db.name) continue; + const dbName = db.name; + try { + await new Promise((resolve, reject) => { + const deleteReq = indexedDB.deleteDatabase(dbName); + const timeout = setTimeout(() => { + console.warn('IndexedDB delete timeout:', dbName); + resolve(); + }, 5000); + deleteReq.onblocked = () => { + console.warn('IndexedDB delete blocked:', dbName); + clearTimeout(timeout); + resolve(); + }; + deleteReq.onsuccess = () => { + clearTimeout(timeout); + resolve(); + }; + deleteReq.onerror = () => { + clearTimeout(timeout); + reject(new Error(`Failed to delete ${dbName}`)); + }; + }); + count++; + } catch (e) { + console.error('Delete DB error:', e); + } + } + return { count }; + }, + 'clear IndexedDB', + ); } export async function injectClearCacheStorage(tabId: number): Promise { - return runCleanScript(tabId, async () => { - if ('caches' in window) { - const cacheNames = await caches.keys(); - for (const name of cacheNames) { - await caches.delete(name); + return runCleanScript( + tabId, + async () => { + if ('caches' in window) { + const cacheNames = await caches.keys(); + for (const name of cacheNames) { + await caches.delete(name); + } + return { count: cacheNames.length }; } - return { count: cacheNames.length }; - } - return { count: 0 }; - }); + return { count: 0 }; + }, + 'clear CacheStorage', + ); } export async function injectUnregisterServiceWorkers(tabId: number): Promise { - return runCleanScript(tabId, async () => { - if ('serviceWorker' in navigator) { - const registrations = await navigator.serviceWorker.getRegistrations(); - for (const registration of registrations) { - await registration.unregister(); + return runCleanScript( + tabId, + async () => { + if ('serviceWorker' in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (const registration of registrations) { + await registration.unregister(); + } + return { count: registrations.length }; } - return { count: registrations.length }; - } - return { count: 0 }; - }); + return { count: 0 }; + }, + 'unregister ServiceWorkers', + ); } export async function clearStorage( @@ -314,7 +331,7 @@ export async function clearStorage( url: string, options: StorageCleanerOptions, ): Promise { - const result: CleaningResult = { success: true }; + const result: CleaningResult = { overallSuccess: true }; if (options.localStorage) { result.localStorage = await injectClearLocalStorage(tabId); @@ -339,7 +356,7 @@ export async function clearStorage( (r): r is StorageCleanResult => r?.success === false, ); if (failures.length > 0) { - result.success = false; + result.overallSuccess = false; } return result; diff --git a/vitest.setup.ts b/vitest.setup.ts index 97bef60..d4f262a 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -125,7 +125,7 @@ const webExtensionMock = { get: vi.fn().mockResolvedValue({}), sendMessage: vi.fn().mockResolvedValue(undefined), create: vi.fn().mockResolvedValue({}), - reload: vi.fn().mockResolvedValue(undefined), // ✅ 承接 MessageAction.RELOAD_TAB 刷新单元测试 + reload: vi.fn().mockResolvedValue(undefined), }, runtime: { id: 'test-extension-id', @@ -202,7 +202,6 @@ afterEach(() => { vi.restoreAllMocks(); }); -// 全局 matchMedia 极客级环境模拟(ThemeModeProvider 依赖) beforeEach(() => { Object.defineProperty(window, 'matchMedia', { writable: true,