From 1b9f6d044d900b6082b092aaf51795364ec91cc9 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:08:27 +0800 Subject: [PATCH] refactor: unify formatBytes usage and simplify useContextMenuData 1. Remove formatSize/formatByteSize delegation functions: - storageCleaner.ts:formatSize (was just formatBytes wrapper) - textStatistics.ts:formatByteSize (was just formatBytes wrapper) - Update all callers to import formatBytes directly from @/utils/format 2. Simplify useContextMenuData hook: - Remove unnecessary useCallback wrapping - Inline checkAndConsumeData logic directly in useEffect - Eliminate callback->effect dependency cycle -69 lines, +67 lines (net -2 lines, but removes 2 indirection layers) Co-Authored-By: Claude Opus 4.8 --- src/pages/JsonTools/JsonConvertSection.tsx | 4 +- src/pages/JsonTools/JsonFormatSection.tsx | 4 +- src/pages/StorageCleaner/OptionItem.tsx | 4 +- src/pages/TextStatistics/index.tsx | 4 +- src/utils/__tests__/storageCleaner.test.ts | 31 +++++----- src/utils/__tests__/textStatistics.test.ts | 13 +++-- src/utils/storageCleaner.ts | 6 -- src/utils/textStatistics.ts | 16 +----- src/utils/useContextMenuData.ts | 66 +++++++++++++--------- 9 files changed, 73 insertions(+), 75 deletions(-) diff --git a/src/pages/JsonTools/JsonConvertSection.tsx b/src/pages/JsonTools/JsonConvertSection.tsx index e35c76b..05360c5 100644 --- a/src/pages/JsonTools/JsonConvertSection.tsx +++ b/src/pages/JsonTools/JsonConvertSection.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useI18n } from '@/utils/chromeI18n'; -import { formatByteSize } from '@/utils/textStatistics'; +import { formatBytes } from '@/utils/format'; import CopyButton from '@/components/CopyButton'; import TextInputArea from '@/components/TextInputArea'; import { validateJson } from '@/utils/jsonFormatter'; @@ -96,7 +96,7 @@ export default function JsonConvertSection({ {t('jsonFormat:originalSize')}:{' '} - {formatByteSize(result.originalBytes)} + {formatBytes(result.originalBytes)} | diff --git a/src/pages/JsonTools/JsonFormatSection.tsx b/src/pages/JsonTools/JsonFormatSection.tsx index 6c2ea44..ee7d169 100644 --- a/src/pages/JsonTools/JsonFormatSection.tsx +++ b/src/pages/JsonTools/JsonFormatSection.tsx @@ -6,7 +6,7 @@ import { type JsonFormatResult, validateJson, } from '@/utils/jsonFormatter'; -import { formatByteSize } from '@/utils/textStatistics'; +import { formatBytes } from '@/utils/format'; import CopyButton from '@/components/CopyButton'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import TextInputArea from '@/components/TextInputArea'; @@ -127,7 +127,7 @@ export default function JsonFormatSection() { {t('jsonFormat:originalSize')}:{' '} - {formatByteSize(result.originalBytes)} + {formatBytes(result.originalBytes)} | diff --git a/src/pages/StorageCleaner/OptionItem.tsx b/src/pages/StorageCleaner/OptionItem.tsx index 88ba89f..f88ad8e 100644 --- a/src/pages/StorageCleaner/OptionItem.tsx +++ b/src/pages/StorageCleaner/OptionItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { formatSize } from '@/utils/storageCleaner'; +import { formatBytes } from '@/utils/format'; import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 引入官方的 Checkbox 原子组件 @@ -53,7 +53,7 @@ export default function OptionItem({ checked ? 'text-primary/70' : 'text-muted-foreground/70', )} > - {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)} + {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatBytes(size)} ) : ( diff --git a/src/pages/TextStatistics/index.tsx b/src/pages/TextStatistics/index.tsx index 438251e..3661285 100644 --- a/src/pages/TextStatistics/index.tsx +++ b/src/pages/TextStatistics/index.tsx @@ -1,5 +1,5 @@ import TextInputArea from '@/components/TextInputArea'; -import { formatByteSize } from '@/utils/textStatistics'; +import { formatBytes } from '@/utils/format'; import { useI18n } from '@/utils/chromeI18n'; import { useTextStatistics } from './useTextStatistics'; import { cn } from '@/lib/utils'; @@ -12,7 +12,7 @@ export default function Index() { { label: t('textStatistics:characters'), value: stats.characters }, { label: t('textStatistics:words'), value: stats.words }, { label: t('textStatistics:lines'), value: stats.lines }, - { label: t('textStatistics:bytes'), value: formatByteSize(stats.bytes) }, + { label: t('textStatistics:bytes'), value: formatBytes(stats.bytes) }, ]; return ( diff --git a/src/utils/__tests__/storageCleaner.test.ts b/src/utils/__tests__/storageCleaner.test.ts index 4b2d6c2..9137a38 100644 --- a/src/utils/__tests__/storageCleaner.test.ts +++ b/src/utils/__tests__/storageCleaner.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { clearCookies, formatSize, isRestrictedUrl } from '@/utils/storageCleaner'; +import { clearCookies, isRestrictedUrl } from '@/utils/storageCleaner'; +import { formatBytes } from '@/utils/format'; describe('storageCleaner utils', () => { describe('isRestrictedUrl', () => { @@ -48,36 +49,36 @@ describe('storageCleaner utils', () => { }); }); - describe('formatSize', () => { + describe('formatBytes', () => { it('should return "0 B" for 0 bytes', () => { - expect(formatSize(0)).toBe('0 B'); + expect(formatBytes(0)).toBe('0 B'); }); it('should format bytes correctly', () => { - expect(formatSize(500)).toBe('500 B'); + expect(formatBytes(500)).toBe('500 B'); }); it('should format kilobytes correctly', () => { - expect(formatSize(1024)).toBe('1.0 KB'); - expect(formatSize(1536)).toBe('1.5 KB'); - expect(formatSize(2048)).toBe('2.0 KB'); + expect(formatBytes(1024)).toBe('1.0 KB'); + expect(formatBytes(1536)).toBe('1.5 KB'); + expect(formatBytes(2048)).toBe('2.0 KB'); }); it('should format megabytes correctly', () => { - expect(formatSize(1048576)).toBe('1.00 MB'); - expect(formatSize(1572864)).toBe('1.50 MB'); - expect(formatSize(5242880)).toBe('5.00 MB'); + expect(formatBytes(1048576)).toBe('1.00 MB'); + expect(formatBytes(1572864)).toBe('1.50 MB'); + expect(formatBytes(5242880)).toBe('5.00 MB'); }); it('should format gigabytes correctly', () => { - expect(formatSize(1073741824)).toBe('1.00 GB'); - expect(formatSize(2147483648)).toBe('2.00 GB'); + expect(formatBytes(1073741824)).toBe('1.00 GB'); + expect(formatBytes(2147483648)).toBe('2.00 GB'); }); it('should handle edge cases', () => { - expect(formatSize(1)).toBe('1 B'); - expect(formatSize(1023)).toBe('1023 B'); - expect(formatSize(1025)).toBe('1.0 KB'); + expect(formatBytes(1)).toBe('1 B'); + expect(formatBytes(1023)).toBe('1023 B'); + expect(formatBytes(1025)).toBe('1.0 KB'); }); }); diff --git a/src/utils/__tests__/textStatistics.test.ts b/src/utils/__tests__/textStatistics.test.ts index 308f348..81d4103 100644 --- a/src/utils/__tests__/textStatistics.test.ts +++ b/src/utils/__tests__/textStatistics.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { formatByteSize, getTextStats } from '@/utils/textStatistics'; +import { formatBytes } from '@/utils/format'; +import { getTextStats } from '@/utils/textStatistics'; describe('textStatistics utils', () => { describe('getTextStats', () => { @@ -49,12 +50,12 @@ describe('textStatistics utils', () => { }); }); - describe('formatByteSize', () => { + describe('formatBytes', () => { it('should format bytes correctly', () => { - expect(formatByteSize(100)).toBe('100 B'); - expect(formatByteSize(0)).toBe('0 B'); - expect(formatByteSize(1024)).toBe('1.0 KB'); - expect(formatByteSize(1024 * 1024)).toBe('1.00 MB'); + expect(formatBytes(100)).toBe('100 B'); + expect(formatBytes(0)).toBe('0 B'); + expect(formatBytes(1024)).toBe('1.0 KB'); + expect(formatBytes(1024 * 1024)).toBe('1.00 MB'); }); }); }); diff --git a/src/utils/storageCleaner.ts b/src/utils/storageCleaner.ts index 53aef67..f40fea2 100644 --- a/src/utils/storageCleaner.ts +++ b/src/utils/storageCleaner.ts @@ -1,10 +1,4 @@ import type { CleaningResult, StorageCleanerOptions, StorageCleanResult } from '@/types/storage'; -import { formatBytes } from './format'; - -/** 格式化字节大小显示(兼容旧接口,内部委托给 formatBytes) */ -export function formatSize(bytes: number): string { - return formatBytes(bytes); -} const RESTRICTED_PROTOCOLS = [ 'chrome:', diff --git a/src/utils/textStatistics.ts b/src/utils/textStatistics.ts index 4cb6f17..ece617b 100644 --- a/src/utils/textStatistics.ts +++ b/src/utils/textStatistics.ts @@ -1,5 +1,3 @@ -import { formatBytes } from './format'; - /** * 文本统计信息接口 */ @@ -35,7 +33,7 @@ export function getTextStats(text: string): TextStats { const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' }); const segments = segmenter.segment(text); for (const segment of segments) { - // isWordLike 为 true 表示该片段是“类词”的(非空格、非标点) + // isWordLike 为 true 表示该片段是"类词"的(非空格、非标点) if (segment.isWordLike) { words++; } @@ -44,7 +42,7 @@ export function getTextStats(text: string): TextStats { // 降级方案:如果不支持 Intl.Segmenter,使用正则匹配英文单词 // 但对中文支持较差 const englishWords = text.match(/\b\w+\b/g) || []; - const chineseChars = text.match(/[\u4e00-\u9fa5]/g) || []; + const chineseChars = text.match(/[一-龥]/g) || []; words = englishWords.length + chineseChars.length; } @@ -57,13 +55,3 @@ export function getTextStats(text: string): TextStats { return { characters, words, lines, bytes }; } - -/** - * 格式化字节大小显示(兼容旧接口,内部委托给 formatBytes) - * - * @param bytes 字节数 - * @returns 格式化后的字符串 - */ -export function formatByteSize(bytes: number): string { - return formatBytes(bytes); -} diff --git a/src/utils/useContextMenuData.ts b/src/utils/useContextMenuData.ts index 71d353e..8c2b1c1 100644 --- a/src/utils/useContextMenuData.ts +++ b/src/utils/useContextMenuData.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { storageUtil } from '@/utils/chromeStorage'; import type { ContextMenuPendingData, PageType } from '@/types/storage'; @@ -23,44 +23,58 @@ export interface UseContextMenuDataOptions { * 3. Hook 会自动从 storage 中读取并消费匹配的数据 */ export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void { - const checkAndConsumeData = useCallback(async () => { - try { - const data = await storageUtil.get(STORAGE_KEY, undefined); - - if (!data) return; - - if (data.featureKey !== featureKey) return; - - if (Date.now() - data.timestamp > CONTEXT_MENU_DATA_EXPIRY_MS) { - await storageUtil.remove(STORAGE_KEY); - return; - } - - await storageUtil.remove(STORAGE_KEY); - - onData(data.payload); - } catch (error) { - console.error('[useContextMenuData] 处理右键菜单数据失败:', error); - } - }, [featureKey, onData]); - useEffect(() => { - checkAndConsumeData(); - }, [checkAndConsumeData]); + const checkAndConsumeData = async () => { + try { + const data = await storageUtil.get(STORAGE_KEY, undefined); + + if (!data) return; + + if (data.featureKey !== featureKey) return; + + if (Date.now() - data.timestamp > CONTEXT_MENU_DATA_EXPIRY_MS) { + await storageUtil.remove(STORAGE_KEY); + return; + } + + await storageUtil.remove(STORAGE_KEY); + + onData(data.payload); + } catch (error) { + console.error('[useContextMenuData] 处理右键菜单数据失败:', error); + } + }; + + void checkAndConsumeData(); + }, [featureKey, onData]); useEffect(() => { const handleStorageChange = (changes: { [key: string]: chrome.storage.StorageChange }) => { if (changes[STORAGE_KEY]) { const newData = changes[STORAGE_KEY].newValue as ContextMenuPendingData | null; if (newData && newData.featureKey === featureKey) { - checkAndConsumeData(); + void (async () => { + try { + const data = await storageUtil.get(STORAGE_KEY, undefined); + if (!data) return; + if (data.featureKey !== featureKey) return; + if (Date.now() - data.timestamp > CONTEXT_MENU_DATA_EXPIRY_MS) { + await storageUtil.remove(STORAGE_KEY); + return; + } + await storageUtil.remove(STORAGE_KEY); + onData(data.payload); + } catch (error) { + console.error('[useContextMenuData] 处理右键菜单数据失败:', error); + } + })(); } } }; chrome.storage.onChanged.addListener(handleStorageChange); return () => chrome.storage.onChanged.removeListener(handleStorageChange); - }, [featureKey, checkAndConsumeData]); + }, [featureKey, onData]); } /**