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 { 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({
<span>
{t('jsonFormat:originalSize')}:{' '}
<span className="font-semibold text-foreground/80">
{formatByteSize(result.originalBytes)}
{formatBytes(result.originalBytes)}
</span>
</span>
<span className="text-border/60">|</span>
+2 -2
View File
@@ -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() {
<span>
{t('jsonFormat:originalSize')}:{' '}
<span className="font-semibold text-foreground/80">
{formatByteSize(result.originalBytes)}
{formatBytes(result.originalBytes)}
</span>
</span>
<span className="text-border/60">|</span>
+2 -2
View File
@@ -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)}
</span>
) : (
<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 { 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 (
+16 -15
View File
@@ -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');
});
});
+7 -6
View File
@@ -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');
});
});
});
-6
View File
@@ -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:',
+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 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);
}
+22 -8
View File
@@ -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,7 +23,8 @@ export interface UseContextMenuDataOptions {
* 3. Hook 会自动从 storage 中读取并消费匹配的数据
*/
export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void {
const checkAndConsumeData = useCallback(async () => {
useEffect(() => {
const checkAndConsumeData = async () => {
try {
const data = await storageUtil.get(STORAGE_KEY, undefined);
@@ -42,25 +43,38 @@ export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOpt
} catch (error) {
console.error('[useContextMenuData] 处理右键菜单数据失败:', error);
}
}, [featureKey, onData]);
};
useEffect(() => {
checkAndConsumeData();
}, [checkAndConsumeData]);
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]);
}
/**