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 <noreply@anthropic.com>
This commit is contained in:
雨霖铃
2026-05-29 21:08:27 +08:00
parent b41cfb2a02
commit 1b9f6d044d
9 changed files with 73 additions and 75 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useI18n } from '@/utils/chromeI18n'; import { useI18n } from '@/utils/chromeI18n';
import { formatByteSize } from '@/utils/textStatistics'; import { formatBytes } from '@/utils/format';
import CopyButton from '@/components/CopyButton'; import CopyButton from '@/components/CopyButton';
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
import { validateJson } from '@/utils/jsonFormatter'; import { validateJson } from '@/utils/jsonFormatter';
@@ -96,7 +96,7 @@ export default function JsonConvertSection({
<span> <span>
{t('jsonFormat:originalSize')}:{' '} {t('jsonFormat:originalSize')}:{' '}
<span className="font-semibold text-foreground/80"> <span className="font-semibold text-foreground/80">
{formatByteSize(result.originalBytes)} {formatBytes(result.originalBytes)}
</span> </span>
</span> </span>
<span className="text-border/60">|</span> <span className="text-border/60">|</span>
+2 -2
View File
@@ -6,7 +6,7 @@ import {
type JsonFormatResult, type JsonFormatResult,
validateJson, validateJson,
} from '@/utils/jsonFormatter'; } from '@/utils/jsonFormatter';
import { formatByteSize } from '@/utils/textStatistics'; import { formatBytes } from '@/utils/format';
import CopyButton from '@/components/CopyButton'; import CopyButton from '@/components/CopyButton';
import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
@@ -127,7 +127,7 @@ export default function JsonFormatSection() {
<span> <span>
{t('jsonFormat:originalSize')}:{' '} {t('jsonFormat:originalSize')}:{' '}
<span className="font-semibold text-foreground/80"> <span className="font-semibold text-foreground/80">
{formatByteSize(result.originalBytes)} {formatBytes(result.originalBytes)}
</span> </span>
</span> </span>
<span className="text-border/60">|</span> <span className="text-border/60">|</span>
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { formatSize } from '@/utils/storageCleaner'; import { formatBytes } from '@/utils/format';
import { useI18n } from '@/utils/chromeI18n'; import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// 引入官方的 Checkbox 原子组件 // 引入官方的 Checkbox 原子组件
@@ -53,7 +53,7 @@ export default function OptionItem({
checked ? 'text-primary/70' : 'text-muted-foreground/70', checked ? 'text-primary/70' : 'text-muted-foreground/70',
)} )}
> >
{isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)} {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatBytes(size)}
</span> </span>
) : ( ) : (
<span className="block text-[10px] font-medium text-muted-foreground/50 mt-0.5 italic"> <span className="block text-[10px] font-medium text-muted-foreground/50 mt-0.5 italic">
+2 -2
View File
@@ -1,5 +1,5 @@
import TextInputArea from '@/components/TextInputArea'; import TextInputArea from '@/components/TextInputArea';
import { formatByteSize } from '@/utils/textStatistics'; import { formatBytes } from '@/utils/format';
import { useI18n } from '@/utils/chromeI18n'; import { useI18n } from '@/utils/chromeI18n';
import { useTextStatistics } from './useTextStatistics'; import { useTextStatistics } from './useTextStatistics';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -12,7 +12,7 @@ export default function Index() {
{ label: t('textStatistics:characters'), value: stats.characters }, { label: t('textStatistics:characters'), value: stats.characters },
{ label: t('textStatistics:words'), value: stats.words }, { label: t('textStatistics:words'), value: stats.words },
{ label: t('textStatistics:lines'), value: stats.lines }, { label: t('textStatistics:lines'), value: stats.lines },
{ label: t('textStatistics:bytes'), value: formatByteSize(stats.bytes) }, { label: t('textStatistics:bytes'), value: formatBytes(stats.bytes) },
]; ];
return ( return (
+16 -15
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'; 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('storageCleaner utils', () => {
describe('isRestrictedUrl', () => { describe('isRestrictedUrl', () => {
@@ -48,36 +49,36 @@ describe('storageCleaner utils', () => {
}); });
}); });
describe('formatSize', () => { describe('formatBytes', () => {
it('should return "0 B" for 0 bytes', () => { 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', () => { it('should format bytes correctly', () => {
expect(formatSize(500)).toBe('500 B'); expect(formatBytes(500)).toBe('500 B');
}); });
it('should format kilobytes correctly', () => { it('should format kilobytes correctly', () => {
expect(formatSize(1024)).toBe('1.0 KB'); expect(formatBytes(1024)).toBe('1.0 KB');
expect(formatSize(1536)).toBe('1.5 KB'); expect(formatBytes(1536)).toBe('1.5 KB');
expect(formatSize(2048)).toBe('2.0 KB'); expect(formatBytes(2048)).toBe('2.0 KB');
}); });
it('should format megabytes correctly', () => { it('should format megabytes correctly', () => {
expect(formatSize(1048576)).toBe('1.00 MB'); expect(formatBytes(1048576)).toBe('1.00 MB');
expect(formatSize(1572864)).toBe('1.50 MB'); expect(formatBytes(1572864)).toBe('1.50 MB');
expect(formatSize(5242880)).toBe('5.00 MB'); expect(formatBytes(5242880)).toBe('5.00 MB');
}); });
it('should format gigabytes correctly', () => { it('should format gigabytes correctly', () => {
expect(formatSize(1073741824)).toBe('1.00 GB'); expect(formatBytes(1073741824)).toBe('1.00 GB');
expect(formatSize(2147483648)).toBe('2.00 GB'); expect(formatBytes(2147483648)).toBe('2.00 GB');
}); });
it('should handle edge cases', () => { it('should handle edge cases', () => {
expect(formatSize(1)).toBe('1 B'); expect(formatBytes(1)).toBe('1 B');
expect(formatSize(1023)).toBe('1023 B'); expect(formatBytes(1023)).toBe('1023 B');
expect(formatSize(1025)).toBe('1.0 KB'); expect(formatBytes(1025)).toBe('1.0 KB');
}); });
}); });
+7 -6
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'; 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('textStatistics utils', () => {
describe('getTextStats', () => { describe('getTextStats', () => {
@@ -49,12 +50,12 @@ describe('textStatistics utils', () => {
}); });
}); });
describe('formatByteSize', () => { describe('formatBytes', () => {
it('should format bytes correctly', () => { it('should format bytes correctly', () => {
expect(formatByteSize(100)).toBe('100 B'); expect(formatBytes(100)).toBe('100 B');
expect(formatByteSize(0)).toBe('0 B'); expect(formatBytes(0)).toBe('0 B');
expect(formatByteSize(1024)).toBe('1.0 KB'); expect(formatBytes(1024)).toBe('1.0 KB');
expect(formatByteSize(1024 * 1024)).toBe('1.00 MB'); expect(formatBytes(1024 * 1024)).toBe('1.00 MB');
}); });
}); });
}); });
-6
View File
@@ -1,10 +1,4 @@
import type { CleaningResult, StorageCleanerOptions, StorageCleanResult } from '@/types/storage'; 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 = [ const RESTRICTED_PROTOCOLS = [
'chrome:', 'chrome:',
+2 -14
View File
@@ -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 segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
const segments = segmenter.segment(text); const segments = segmenter.segment(text);
for (const segment of segments) { for (const segment of segments) {
// isWordLike 为 true 表示该片段是类词的(非空格、非标点) // isWordLike 为 true 表示该片段是"类词"的(非空格、非标点)
if (segment.isWordLike) { if (segment.isWordLike) {
words++; words++;
} }
@@ -44,7 +42,7 @@ export function getTextStats(text: string): TextStats {
// 降级方案:如果不支持 Intl.Segmenter,使用正则匹配英文单词 // 降级方案:如果不支持 Intl.Segmenter,使用正则匹配英文单词
// 但对中文支持较差 // 但对中文支持较差
const englishWords = text.match(/\b\w+\b/g) || []; 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; words = englishWords.length + chineseChars.length;
} }
@@ -57,13 +55,3 @@ export function getTextStats(text: string): TextStats {
return { characters, words, lines, bytes }; return { characters, words, lines, bytes };
} }
/**
* 格式化字节大小显示(兼容旧接口,内部委托给 formatBytes
*
* @param bytes 字节数
* @returns 格式化后的字符串
*/
export function formatByteSize(bytes: number): string {
return formatBytes(bytes);
}
+40 -26
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react'; import { useEffect } from 'react';
import { storageUtil } from '@/utils/chromeStorage'; import { storageUtil } from '@/utils/chromeStorage';
import type { ContextMenuPendingData, PageType } from '@/types/storage'; import type { ContextMenuPendingData, PageType } from '@/types/storage';
@@ -23,44 +23,58 @@ export interface UseContextMenuDataOptions {
* 3. Hook 会自动从 storage 中读取并消费匹配的数据 * 3. Hook 会自动从 storage 中读取并消费匹配的数据
*/ */
export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void { 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(() => { useEffect(() => {
checkAndConsumeData(); const checkAndConsumeData = async () => {
}, [checkAndConsumeData]); 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(() => { useEffect(() => {
const handleStorageChange = (changes: { [key: string]: chrome.storage.StorageChange }) => { const handleStorageChange = (changes: { [key: string]: chrome.storage.StorageChange }) => {
if (changes[STORAGE_KEY]) { if (changes[STORAGE_KEY]) {
const newData = changes[STORAGE_KEY].newValue as ContextMenuPendingData | null; const newData = changes[STORAGE_KEY].newValue as ContextMenuPendingData | null;
if (newData && newData.featureKey === featureKey) { 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); chrome.storage.onChanged.addListener(handleStorageChange);
return () => chrome.storage.onChanged.removeListener(handleStorageChange); return () => chrome.storage.onChanged.removeListener(handleStorageChange);
}, [featureKey, checkAndConsumeData]); }, [featureKey, onData]);
} }
/** /**