# 代码编写规范 本文档定义了 Testing Tools 浏览器扩展项目的编码规范和最佳实践。所有代码贡献者应遵循这些规范以保持代码库的一致性和可维护性。 ## 1. TypeScript 规范 ### 1.1 类型定义:`interface` vs `type` - **`interface`**:用于组件 Props、对象结构、Context 类型等可扩展结构 - **`type`**:用于联合类型、工具类型、不可扩展的类型别名 ```typescript // ✅ interface — 组件 Props / 对象结构 export interface GlobalSnackbarProps { message: string; open: boolean; onClose: () => void; severity?: SnackbarSeverity; } // ✅ interface — 继承 HTML 属性 interface LiveClockProps extends React.HTMLAttributes { unit: UnitType; onUseNow: (val: number) => void; } // ✅ type — 联合类型 export type ThemeMode = 'light' | 'dark' | 'system'; export type PageType = 'dashboard' | 'timestamp' | 'storageCleaner' | ...; // ✅ type — 工具类型 export type ResolvedThemeMode = 'light' | 'dark'; ``` ### 1.2 泛型使用 广泛使用泛型约束,结合 `extends` 进行类型守卫: ```typescript // ✅ 泛型 + extends 约束 export interface SwitchOption { value: T; label: React.ReactNode; } // ✅ 泛型 + StorageSchema 键约束 async get( key: K, defaultValue?: StorageSchema[K], ): Promise { ... } // ✅ 泛型 Hook export const useStorageState = ( key: K, defaultValue: StorageSchema[K], validator?: (val: unknown) => val is StorageSchema[K], ) => { ... } ``` ### 1.3 类型守卫 优先使用类型守卫函数(`val is Type` 谓词),避免 `as` 强转: ```typescript // ✅ 类型守卫谓词函数 const isValidMode = (v: unknown): v is ThemeMode => VALID_MODES.includes(v as ThemeMode); const isValidPage = (page: unknown): page is PageType => { return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page); }; // ✅ 安全的 as 断言,仅在类型守卫验证后使用 export function isSupportedImageType(mimeType: string): boolean { return (SUPPORTED_IMAGE_TYPES as readonly string[]).includes(mimeType); } ``` ### 1.4 导出模式 | 场景 | 导出方式 | 示例 | | ----------- | ------------------------------------------------ | --------------------------------- | | 页面组件 | `export default function ComponentName()` | `src/pages/Timestamp/index.tsx` | | 业务组件 | `const X = React.memo(...)` + `export default X` | `LiveClock.tsx`, `ResultView.tsx` | | UI 原子组件 | `React.forwardRef(...)` + `export { X }` | `src/components/ui/button.tsx` | | 工具函数 | `export function xxx()` | `src/utils/clipboard.ts` | | 自定义 Hook | `export function useXxx()` | `src/utils/useStorageState.ts` | | 类型/接口 | `export interface` / `export type` | `src/types/storage.d.ts` | ```typescript // ✅ 页面组件 — default export export default function Index() { ... } // ✅ 需要 memo 的组件 — 箭头函数 + React.memo + displayName const LiveClock = React.memo(({ ... }: LiveClockProps) => { ... }); LiveClock.displayName = 'LiveClock'; export default LiveClock; // ✅ UI 组件 — forwardRef + 命名导出 const Button = React.forwardRef(...); Button.displayName = 'Button'; export { Button, buttonVariants }; ``` --- ## 2. React 组件规范 ### 2.1 组件定义方式 - **标准组件**:使用 `function` 声明 - **需要 memo 的组件**:使用箭头函数 + `React.memo` - **需要 ref 的组件**:使用 `React.forwardRef` - **错误边界**:使用 Class 组件(React 要求) ```typescript // ✅ 标准页面组件 export default function Index() { ... } // ✅ 需要 memo 的组件 const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => { ... }); LiveClock.displayName = 'LiveClock'; export default LiveClock; // ✅ 需要 ref 的组件 const TextInputArea = forwardRef((props, ref) => { ... }); TextInputArea.displayName = 'TextInputArea'; export default TextInputArea; // ✅ Class 组件(仅用于 ErrorBoundary) export class ErrorBoundary extends Component { ... } ``` ### 2.2 Props 模式 - 使用 `interface` 定义 Props - 继承 `React.HTMLAttributes` 以支持原生属性透传 - 使用 `Omit` 排除冲突属性 - 解构 `className` 和 `...rest props` ```typescript // ✅ 继承 HTML 属性 + className 透传 interface ResultViewProps extends React.HTMLAttributes { result: string; mode: 'ts2dt' | 'dt2ts'; unit: UnitType; zone: string; showEmptyPlaceholder?: boolean; } // 使用时解构 className 和 rest props const ResultView = React.memo(({ result, mode, unit, zone, showEmptyPlaceholder = false, className, ...props }: ResultViewProps) => { return
...
; }); // ✅ Omit 排除冲突属性 export interface TextInputAreaProps extends Omit< React.TextareaHTMLAttributes, 'onChange' > { ... } ``` ### 2.3 状态管理 - **本地状态**:`useState` + 惰性初始化 - **衍生状态**:`useMemo` 响应式计算管线 - **持久化状态**:Chrome Storage + `localStorage` 快照 - **全局状态**:React Context ```typescript // ✅ useState + 惰性初始化 const [input, setInput] = useState(() => String(Date.now())); // ✅ useMemo 响应式计算管线(零延迟,无需手动 convert 按钮) const conversionPipeline = useMemo(() => { const rawInput = input.trim(); if (!rawInput) return { result: '', error: '' }; // ... 自动计算结果 }, [input, mode, unit, zone, t]); // ✅ Chrome Storage 持久化状态 export const useStorageState = ( key: K, defaultValue: StorageSchema[K], validator?: ... ) => { ... } ``` ### 2.4 副作用模式 - **取消标志**:防止异步竞态 - **ref 回调指针**:保持回调最新避免依赖膨胀 - **事件监听 cleanup**:始终在 cleanup 中移除监听器 - **定时器 cleanup**:始终在 cleanup 中清除定时器 ```typescript // ✅ 取消标志模式 — 防止异步竞态 useEffect(() => { let cancelled = false; storageUtil.get(THEME_MODE_KEY, 'system').then((saved) => { if (cancelled) return; if (isValidMode(saved)) { ... } }); return () => { cancelled = true; }; }, [updateResolved]); // ✅ ref 回调指针 — 保持回调最新避免依赖膨胀 const onUseNowRef = useRef(onUseNow); useEffect(() => { onUseNowRef.current = onUseNow; }, [onUseNow]); // ✅ setInterval + cleanup useEffect(() => { const tickId = setInterval(tick, 200); return () => clearInterval(tickId); }, [unit]); // ✅ 事件监听 cleanup useEffect(() => { const handleClickOutside = (event: MouseEvent) => { ... }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); ``` ### 2.5 `memo` / `useCallback` / `useMemo` 使用 | 场景 | 使用方式 | | ---------------------------------- | ------------- | | 高频渲染组件(列表子项、实时时钟) | `React.memo` | | 事件处理函数、回调引用 | `useCallback` | | 响应式计算管线、衍生数据 | `useMemo` | | 避免重复创建对象/集合 | `useMemo` | ```typescript // ✅ React.memo — 高频更新组件 const LiveClock = React.memo(({ ... }) => { ... }); const ResultView = React.memo(({ ... }) => { ... }); // ✅ useCallback — 事件处理 const handleUseNow = useCallback((now: number) => { if (mode === 'ts2dt') { setInput(String(unit === 'ms' ? now : Math.floor(now / 1000))); } else { setInput(dayjs(now).tz(zone).format(DATE_FORMAT)); } }, [mode, unit, zone]); // ✅ useMemo — 避免重复创建集合 const visibleSet = useMemo(() => new Set(visiblePages), [visiblePages]); ``` --- ## 3. 导入规范 ### 3.1 导入顺序 按来源分组,顺序如下: 1. React 核心 2. 第三方库(图标、UI 库等) 3. 业务 Provider / Context 4. 配置 / 存储 5. 本地页面组件 6. UI 组件 7. 工具函数 / Hook 8. 类型 9. 常量 ```typescript // 1. React 核心 import React, { useEffect, useMemo, useRef, useState } from 'react'; // 2. 第三方库 import { ArrowLeft, ExternalLink, Globe } from 'lucide-react'; import { toast } from 'sonner'; // 3. 业务 Provider import { useRouter } from '@/providers/RouterProvider'; import { useThemeMode } from '@/providers/ThemeModeProvider'; // 4. 配置 / 存储 import { FeatureConfig, FEATURES } from '@/config/features'; import { storageUtil } from '@/utils/chromeStorage'; // 5. 本地组件 import TextMode from './TextMode'; import { ZONES } from './constants'; // 6. UI 组件 import SwitchButtonGroup from '@/components/SwitchButtonGroup'; import { Button } from '@/components/ui/button'; // 7. 工具函数 / Hook import { cn } from '@/lib/utils'; import { useStorageState } from '@/utils/useStorageState'; // 8. 类型 import type { PageType, StorageSchema } from '@/types/storage'; ``` ### 3.2 路径别名 - `@/` 映射到项目根目录 - **跨目录导入**:使用 `@/` 绝对别名 - **同目录导入**:使用相对路径 `./` ```typescript // ✅ 绝对别名导入 — 跨目录 import { cn } from '@/lib/utils'; import { storageUtil } from '@/utils/chromeStorage'; import type { StorageSchema } from '@/types/storage'; import { Button } from '@/components/ui/button'; // ✅ 相对导入 — 仅限同目录 import TextMode from './TextMode'; import { ZONES } from './constants'; import { useTimestampConverter } from './useTimestampConverter'; ``` --- ## 4. 样式规范 ### 4.1 `cn()` 工具函数 统一使用 `cn()` 合并 Tailwind 类名(来自 `clsx` + `tailwind-merge`),导入自 `@/lib/utils`: ```typescript import { cn } from '@/lib/utils'; // ✅ 条件类名 + 合并外部 className
// ✅ 错误状态变体 // ✅ 选中/未选中状态 className={cn( 'flex-1 inline-flex items-center justify-center font-medium whitespace-nowrap transition-all', sizeClasses[size], isSelected ? 'bg-background text-foreground shadow-sm font-semibold' : 'hover:bg-background/50 hover:text-foreground/80', buttonClassName, )} ``` ### 4.2 主题 / 暗色模式 使用 shadcn/ui 的 CSS 变量语义化类名,**禁止硬编码颜色值**: ```typescript // ✅ 语义化颜色 token — 亮/暗模式自适应
// ✅ 暗色模式特殊处理 'fixed ... bg-white dark:bg-gray-900 p-6 ...' // ✅ 需要固定颜色的特殊场景(如二维码白色背景保护)
``` **常用语义化 token:** | 用途 | 类名 | | ---- | ------------------------------------------------------------------ | | 背景 | `bg-background`, `bg-card`, `bg-muted`, `bg-secondary` | | 文字 | `text-foreground`, `text-card-foreground`, `text-muted-foreground` | | 边框 | `border-border`, `border-border/80` | | 主色 | `text-primary`, `bg-primary`, `border-primary` | | 危险 | `text-destructive`, `bg-destructive`, `border-destructive` | ### 4.3 响应式设计 移动优先,使用 `sm:` / `md:` / `lg:` 断点: ```typescript // ✅ Grid 自适应布局
// ✅ Dashboard 自动填充网格
// ✅ 弹性方向切换
// ✅ 内边距响应式
``` --- ## 5. 错误处理 ### 5.1 工具函数:可恢复错误返回可判断结果 可恢复的解析/校验错误应返回可判断的结果,避免工具层直接弹 Toast。确需保留底层异常的函数 (如 `formatJson` / `minifyJson`)必须在页面 Hook 或 UI 层捕获并转换为用户提示: ```typescript // ✅ 可恢复校验返回错误消息,调用方据此展示 UI export function validateJson(text: string): string | null { if (!text.trim()) { return null; } try { JSON.parse(text.trim()); return null; } catch (e) { return e instanceof SyntaxError ? e.message : 'Invalid JSON'; } } ``` ### 5.2 UI 层:try-catch + Toast UI 层异步操作使用 try-catch,通过 `sonner` 的 `toast` 显示错误: ```typescript import { toast } from 'sonner'; const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(value); toast.success('复制成功'); } catch { toast.error('复制失败'); } }, [value]); ``` ### 5.3 Promise 异常隔离 对不关心返回值的异步操作,使用 `void` + `.catch()` 隔离异常: ```typescript // ✅ void + .catch 模式 void storageUtil.set(THEME_MODE_KEY, next).catch((err) => { console.error('[Theme Storage Error] Failed to persistent theme state:', err); }); // ✅ async 函数调用 + .catch loadConfig().catch(console.error); ``` ### 5.4 ErrorBoundary 在应用顶层使用 `ErrorBoundary` 组件捕获子组件树异常: ```typescript import ErrorBoundary from '@/components/ErrorBoundary'; ``` --- ## 6. 命名规范 ### 6.1 文件命名 | 类型 | 命名模式 | 示例 | | ----------- | -------------------------- | -------------------------------------------------------- | | 页面目录 | PascalCase | `Timestamp/`, `Base64Converter/`, `StorageCleaner/` | | 页面入口 | `index.tsx` | `src/pages/Timestamp/index.tsx` | | 组件文件 | PascalCase `.tsx` | `TopBar.tsx`, `LiveClock.tsx`, `ResultView.tsx` | | 自定义 Hook | camelCase `.ts` | `useTimestampConverter.ts`, `useStorageCleaner.ts` | | 工具函数 | camelCase `.ts` | `chromeStorage.ts`, `base64Converter.ts`, `clipboard.ts` | | 测试文件 | 与源文件同名 `.test.ts(x)` | `jwt.test.ts`, `SwitchButtonGroup.test.tsx` | | 类型文件 | camelCase `.d.ts` | `storage.d.ts` | | 常量文件 | camelCase `.ts` | `constants.ts` | ### 6.2 变量 / 函数命名 ```typescript // ✅ camelCase — 变量、函数、Hook const conversionPipeline = useMemo(...); const handleUseNow = useCallback(...); export function useTimestampConverter(): UseTimestampConverterReturn { ... } export function textToBase64(text: string): TextToBase64Result { ... } // ✅ PascalCase — 组件、类型、接口 const LiveClock = React.memo(...); export interface GlobalSnackbarProps { ... } export type ThemeMode = 'light' | 'dark' | 'system'; // ✅ SCREAMING_SNAKE_CASE — 常量 const SEARCH_HISTORY_LIMIT = 10; const THEME_MODE_KEY = 'app/themeMode' as const; export const MAX_FILE_SIZE = 10 * 1024 * 1024; export const SUPPORTED_IMAGE_TYPES = [...] as const; // ✅ 布尔值 — is/has/should 前缀 const isControlled = controlledValue !== undefined; const isDashboard = currentPage === 'dashboard'; const hasError = true; ``` ### 6.3 事件处理函数 使用 `handle` 前缀命名组件内事件处理函数: ```typescript const handleUseNow = useCallback((now: number) => { ... }, []); const handleSelectFeature = (feature: FeatureConfig) => { ... }; const handleFileChange = useCallback((file: File) => { ... }, []); const handleClean = useCallback(async () => { ... }, []); ``` --- ## 7. 测试规范 ### 7.1 文件组织 测试文件放在源代码同级的 `__tests__/` 目录下: ``` src/utils/__tests__/jwt.test.ts src/utils/__tests__/base64Converter.test.ts src/utils/__tests__/useStorageState.test.ts src/components/__tests__/SwitchButtonGroup.test.tsx src/components/__tests__/ErrorBoundary.test.tsx src/pages/Timestamp/__tests__/index.test.tsx ``` ### 7.2 describe / it 命名 `describe` 使用模块/函数名,`it` 使用中文描述行为("应该..."): ```typescript // ✅ 中文 describe + 中文 it describe('textToBase64', () => { it('应该编码 ASCII 文本', () => { ... }); it('应该编码中文文本', () => { ... }); it('应该编码空字符串', () => { ... }); }); // ✅ 中文 describe + 中文 it(组件测试) describe('SwitchButtonGroup 组件', () => { it('应渲染所有选项按钮', () => { ... }); it('应高亮当前选中的按钮', () => { ... }); it('点击未选中按钮时应触发 onChange 并传入选中值', () => { ... }); }); ``` ### 7.3 Mock 模式 - 使用 `vi.mock()` 进行模块级 Mock - 使用 `vi.fn()` 进行函数级 Mock - 使用 `vi.useFakeTimers()` 控制时间 - **避免重复 mock `vitest.setup.ts` 中已有的内容**(chrome API、matchMedia 等) ```typescript // ✅ 模块级 Mock vi.mock('@/utils/chromeStorage', () => ({ storageUtil: { get: vi.fn(), set: vi.fn(() => Promise.resolve()), }, })); // ✅ 函数级 Mock + 断言 const handleChange = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: /选项B/i })); expect(handleChange).toHaveBeenCalledTimes(1); expect(handleChange).toHaveBeenCalledWith('b'); // ✅ 定时器 Mock beforeEach(() => { vi.useFakeTimers(); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); ``` ### 7.4 断言模式 使用 Testing Library 的 DOM 查询 + Vitest 匹配器: ```typescript // ✅ 语义化查询 expect(screen.getByRole('button', { name: /选项A/i })).toBeInTheDocument(); expect(screen.getByTestId('normal-content')).toHaveTextContent('正常内容'); expect(screen.queryByText('糟糕,出了点问题')).not.toBeInTheDocument(); // ✅ CSS 类断言 expect(button).toHaveClass('bg-background', 'text-foreground', 'shadow-sm'); // ✅ 异步断言 await waitFor(() => { expect(result.current[0]).toBe(false); expect(result.current[2]).toBe(true); }); // ✅ renderHook 测试自定义 Hook const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true)); expect(result.current[0]).toBe(true); ``` --- ## 8. 自定义 Hook 规范 ### 8.1 命名和结构 - 使用 `use` 前缀命名 - 定义返回值接口类型 - 添加 JSDoc 注释 ```typescript // ✅ 完整的 Hook 结构 /** * 自定义 Hook:处理右键菜单传递的数据 * * @param options - 配置选项 * @example * useContextMenuData({ featureKey: 'jwt', onData: handlePayload }); */ export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void { const checkAndConsumeData = useCallback(async () => { ... }, [featureKey, onData]); useEffect(() => { checkAndConsumeData(); }, [checkAndConsumeData]); } // ✅ 返回值接口定义 export interface UseTimestampConverterReturn { mode: 'ts2dt' | 'dt2ts'; input: string; result: string; error: string; setMode: (mode: 'ts2dt' | 'dt2ts') => void; setInput: (value: string) => void; handleUseNow: (now: number) => void; } export function useTimestampConverter(): UseTimestampConverterReturn { ... } ``` ### 8.2 Hook 存放位置 - **全局通用 Hook**:放在 `src/utils/` 目录下 - **页面专属 Hook**:与页面组件同目录 ``` src/utils/useStorageState.ts — Chrome Storage 状态持久化 src/utils/syncSnapshot.ts — localStorage 快照读取 src/utils/useContextMenuData.ts — 右键菜单数据 src/utils/useDebounce.ts — 防抖 src/pages/Timestamp/useTimestampConverter.ts — 页面级 Hook src/pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook ``` --- ## 9. UI 文案规范 项目已移除 `chrome.i18n`,所有 UI 文案直接在代码中使用中文。 ### 9.1 功能元数据 功能名称与描述在 `config/features.tsx` 的 `FEATURES` 数组中定义: ```typescript { key: 'timestamp', label: '时间戳转换', description: '日期与时间戳互转', defaultVisible: true, components: { popup: TimestampPage, sidepanel: TimestampPage, tab: TimestampPage }, } ``` Dashboard 卡片、TopBar 搜索等功能从此处读取 `label` / `description`。 ### 9.2 页面与组件文案 - 页面标题、按钮、提示信息等直接在 JSX 或 `constants.ts` 中写中文 - 错误消息可在 Hook 中定义,或使用常量映射 - Toast 通知使用 `sonner` 的 `toast()`,文案写在调用处或常量中 ```typescript // ✅ 页面组件 — 直接写中文 export default function Index() { return

时间戳转换

; } // ✅ 常量文件 — 可复用文案 export const ERROR_MESSAGES = { invalidInput: '输入格式无效', conversionFailed: '转换失败', } as const; ``` ### 9.3 添加新功能文案 1. 在 `config/features.tsx` 填写 `label` 和 `description` 2. 在页面组件、`constants.ts` 或 Hook 中编写 UI 文案 3. 扩展名称与描述在 `wxt.config.ts` 的 `manifest` 中维护 --- ## 10. 存储规范 ### 10.1 StorageSchema 所有 Chrome Storage 键必须在 `types/storage.d.ts` 的 `StorageSchema` 中声明: ```typescript export interface StorageSchema { 'app/currentRoute': PageType; 'app/popupRoute': PageType; 'app/theme': string; 'app/themeMode': 'light' | 'dark' | 'system'; 'storageCleaner/preferences': StorageCleanerPreferences; // ... } ``` ### 10.2 存储操作 使用 `utils/chromeStorage.ts` 的类型安全封装: ```typescript import { storageUtil } from '@/utils/chromeStorage'; import type { StorageSchema } from '@/types/storage'; // ✅ 读取 const theme = await storageUtil.get('app/theme', 'default'); // ✅ 写入 await storageUtil.set('app/theme', 'dark'); // ✅ 删除 await storageUtil.remove('app/theme'); ``` ### 10.3 持久化状态 Hook 使用 `useStorageState` 自动同步 Chrome Storage: ```typescript import { useStorageState } from '@/utils/useStorageState'; const [themeMode, setThemeMode, isInitialized] = useStorageState( 'app/themeMode', 'system', isValidMode, // 可选的类型守卫 ); ``` ### 10.4 首屏快照与初始化防覆盖 Chrome Storage 读取是异步的。项目通过 `localStorage` 快照(键名 `snapshot/{storageKey}`)提供同步初始值,消除首屏闪烁。 | 模块 | 快照工具 | 防覆盖机制 | | ---- | -------- | ---------- | | `RouterProvider` | `syncSnapshot.ts` | `canPersistRef`(加载成功后才写入)、`hasUserNavigatedRef`(用户导航后不被 storage 覆盖) | | `useStorageState` | `syncSnapshot.ts` | `loadSucceededRef` 或 `userModifiedRef` 为 true 时才写入 | | `ThemeModeProvider` | `themeSnapshot.ts` | `hasUserSetMode`(用户切换主题后不被 storage 覆盖) | 新增持久化状态时,应遵循相同模式:同步快照作初始 state → 异步加载 storage → 加载成功或用户修改后才允许写入。 --- ## 11. 页面开发规范 ### 11.1 目录结构(按复杂度分级) #### 简单页面(单一功能,无子模式) 适用于 Timestamp、Jwt、TextStatistics、RightClickRestorer 等: ``` src/pages/FeatureName/ ├── index.tsx # 页面入口组件(default export) ├── useFeatureName.ts # 业务逻辑 Hook(命名导出) ├── constants.ts # 常量定义(可选,命名导出) ├── SubComponent.tsx # 子组件(可选,default export) └── __tests__/ └── index.test.tsx # 页面集成测试 ``` #### 中等页面(含多个子模式/标签页切换) 适用于 Base64Converter、StorageCleaner 等: ``` src/pages/FeatureName/ ├── index.tsx # 页面入口(模式路由 + 顶层布局) ├── useFeatureName.ts # 业务逻辑 Hook(命名导出) ├── SubModeA.tsx # 子模式组件 ├── SubModeB.tsx # 子模式组件 ├── SubComponent.tsx # 可复用子组件 └── __tests__/ ├── index.test.tsx └── SubModeA.test.tsx ``` #### 复杂页面(Context + 多组件协作) 适用于 QrCode、JsonTools 等: ``` src/pages/FeatureName/ ├── index.tsx # 页面入口(Provider + 布局) ├── types.ts # 页面专属类型定义 ├── constants.ts # 常量(可选) ├── contexts/ # React Context 定义 │ └── FeatureContext.ts ├── hooks/ # 页面专属 Hooks │ └── useFeature.ts ├── components/ # 页面专属子组件 │ ├── PanelA.tsx │ └── PanelB.tsx └── __tests__/ ├── index.test.tsx └── useFeature.test.ts ``` #### 特殊情况(单个文件即可) 功能极简的页面(如 Dashboard),仅需 `index.tsx` 一个文件。当 `index.tsx` 超过 **150 行**时,应拆分为 UI + Hook 模式。 --- ### 11.2 页面入口组件(`index.tsx`)规范 #### 组件命名 - 页面入口组件**统一使用 `Index` 作为函数名**,通过 `export default` 导出 - 使用 `export default function Index()` 而非匿名默认导出 - **禁止**混用 `XxxPage` 命名(当前 `RightClickRestorerPage`、`DashboardPage` 不合规范,应统一为 `Index`) ```typescript // ✅ 正确 export default function Index() { ... } // ❌ 错误 — 命名不一致 export default function RightClickRestorerPage() { ... } export default function DashboardPage() { ... } ``` #### 组件职责 `index.tsx` 只负责两件事: 1. **调用业务 Hook** 获取状态和操作方法 2. **渲染 UI 布局**(纯展示,无业务逻辑) ```typescript // ✅ 标准页面入口模板 import { useFeatureName } from './useFeatureName'; export default function Index() { const { state, actions } = useFeatureName(); return (
{/* 纯 UI 渲染,文案直接写中文 */}
); } ``` #### 禁止在 `index.tsx` 中编写的内容 - ❌ `useState` / `useMemo` / `useCallback`(应放在 Hook 中) - ❌ 数据转换/格式化逻辑 - ❌ 异步请求/副作用 - ❌ 超过 3 行的条件判断逻辑 --- ### 11.3 业务 Hook 规范(`useFeatureName.ts`) #### 命名 - 文件名:`useXxx.ts`(驼峰命名) - Hook 函数名:`useXxx()` - 返回值接口:`UseXxxReturn` ```typescript // ✅ 标准 Hook 结构 export interface UseTimestampConverterReturn { mode: 'ts2dt' | 'dt2ts'; input: string; result: string; error: string; setMode: (mode: 'ts2dt' | 'dt2ts') => void; setInput: (value: string) => void; handleUseNow: (now: number) => void; } export function useTimestampConverter(): UseTimestampConverterReturn { // 所有业务逻辑在此 } ``` #### Hook 内部结构(推荐顺序) ```typescript export function useFeatureName(): UseFeatureNameReturn { // 1. 基础 state(useState) const [mode, setMode] = useState('default'); const [input, setInput] = useState(''); // 2. 持久化 state(useStorageState) const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'default', isValidMode); // 3. 衍生数据(useMemo)— 响应式计算管线 const result = useMemo(() => { // 自动计算,无需手动点击"转换"按钮 }, [input, mode]); // 4. 事件处理(useCallback) const handleAction = useCallback(() => { ... }, [deps]); // 5. 副作用(useEffect)— 防抖、初始化、清理 useEffect(() => { ... }, [deps]); // 7. 右键菜单数据(页面需要时) useContextMenuData({ featureKey: 'featureName', onData: handleContextMenuData }); // 8. 返回 return { mode, input, result, setMode, setInput, handleAction }; } ``` #### 防抖模式 当输入框需要防抖时,在 Hook 中实现: ```typescript // ✅ 防抖管道 — 在 useMemo 前定义 const [input, setInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState(''); useEffect(() => { const handle = setTimeout(() => setDebouncedInput(input), 250); return () => clearTimeout(handle); }, [input]); // 后续 useMemo 使用 debouncedInput 而非 input const result = useMemo(() => compute(debouncedInput), [debouncedInput]); ``` --- ### 11.4 常量文件规范(`constants.ts`) - 仅在常量超过 **3 个**或需要**导出类型**时创建 - 使用 `as const` 确保字面量类型 - 从 `as const` 数组派生联合类型 ```typescript // ✅ 标准常量文件 export const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss'; export const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const; export type UnitType = 'ms' | 's'; export type ZoneType = (typeof ZONES)[number]; ``` --- ### 11.5 子组件规范 #### 何时拆分子组件 - `index.tsx` 超过 **150 行** - 存在可复用的 UI 片段(如卡片、面板、结果展示区) - 需要 `React.memo` 优化的高频渲染区域 #### 子组件 Props 模式 ```typescript // ✅ 继承 HTML 属性 + 业务 Props interface ResultViewProps extends React.HTMLAttributes { result: string; mode: 'ts2dt' | 'dt2ts'; showEmptyPlaceholder?: boolean; } // ✅ 使用 React.memo + displayName const ResultView = React.memo( ({ result, mode, showEmptyPlaceholder = false, className, ...props }: ResultViewProps) => { // 文案直接写中文或使用 constants // ... }, ); ResultView.displayName = 'ResultView'; export default ResultView; ``` #### 子组件内可以使用 Hook 子组件可以独立调用 `useRouter`、`useThemeMode`、`toast` 等全局 Hook/API,**不需要**通过 props 从父组件传递。 --- ### 11.6 存储键命名规范 页面使用的 Storage 键必须遵循 kebab-case 格式:`{功能名}/{用途}`。 ```typescript // ✅ 正确 'base64Converter/pageMode'; 'base64Converter/fileMode/direction'; 'jsonTools/pageMode'; 'qrCode/urlExpanded'; // ❌ 错误 'base64ConverterPageMode'; 'json_tools_page_mode'; ``` 在 `types/storage.d.ts` 的 `StorageSchema` 中声明所有键。 --- ### 11.7 模式切换通用模式 当页面有多个子模式(标签页切换),统一使用以下模式: ```typescript // ✅ 标准模式切换 const VALID_MODES = ['modeA', 'modeB'] as const; type PageMode = (typeof VALID_MODES)[number]; const isValidMode = (val: unknown): val is PageMode => typeof val === 'string' && (VALID_MODES as readonly string[]).includes(val); export default function Index() { const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'modeA', isValidMode); return (
setPageMode(v)} size="small" /> {pageMode === 'modeA' ? : }
); } ``` --- ### 11.8 页面布局约定 - 所有页面根元素使用统一的外层容器: ```
``` - 不需要 `
` — 该样式已由 `AppRoot` 提供 - 不需要 `min-h-[500px]` 或固定高度(除非确有必要) - 卡片容器:`rounded-xl border border-border bg-card text-card-foreground shadow-sm` - 使用 `space-y-4` 管理纵向间距,不要手动 `mb-4` --- ### 11.9 页面开发检查清单 新增功能页面时,逐项确认: 1. ✅ 在 `types/storage.d.ts` 添加 `PageType` 联合类型 2. ✅ 在 `config/features.tsx` 注册 `FEATURES` 配置(key、label、description、icon、三种渲染模式组件) 3. ✅ 创建页面目录,使用 `Index` 作为组件名 4. ✅ 业务逻辑提取到 `useXxx.ts` Hook(index.tsx 不超过 150 行) 5. ✅ 需要持久化的 UI 状态使用 `useStorageState` 6. ✅ 常量 ≥3 个时提取到 `constants.ts` 7. ✅ 在页面组件或 `constants.ts` 中编写 UI 文案 8. ✅ 创建 `__tests__/index.test.tsx` 测试文件 9. ✅ 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions` 10. ✅ 运行 `npm run lint && npm run typecheck && npm run test` 全部通过 --- ### 11.10 目录职责总览 | 目录 | 职责 | | -------------------- | ------------------------------------------------------------ | | `src/config/` | 应用配置(功能定义、路由映射) | | `src/entrypoints/` | 扩展入口点(popup、options、sidepanel、background、content) | | `src/pages/` | 功能页面组件(懒加载) | | `src/components/` | 可复用 UI 组件 | | `src/components/ui/` | shadcn/ui 基础组件(button、dialog、select 等) | | `src/providers/` | React Context(Router、Theme 等) | | `src/hooks/` | 自定义 React Hooks | | `src/utils/` | 工具函数与服务抽象 | | `src/types/` | TypeScript 类型声明 | | `src/lib/` | 通用工具函数与生成器库(cn、utils、generators) | | `public/` | 静态资源(图标等) | --- ## 12. 代码风格 ### 12.1 Prettier 配置 - 行宽:100 字符 - 引号:单引号 - 尾逗号:all - 换行符:LF - 分号:是 ### 12.2 ESLint 规则 - 禁止使用 `any`(测试文件除外) - 未使用变量/参数:使用 `_` 前缀(如 `_unused`) - React 19 JSX Runtime:无需手动导入 React - 使用 `typescript-eslint` 的 `projectService: true` ### 12.3 注释规范 - **文件级注释**:使用 JSDoc `@module` 格式(如 `GlobalSnackbar.tsx`) - **函数注释**:使用 JSDoc,包含 `@param`、`@returns`、`@example` - **行内注释**:仅在需要澄清复杂逻辑时使用 - **禁止注释显而易见的代码**